diff --git a/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/TargetMap.java b/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/TargetMap.java index 9486644a..b4059564 100644 --- a/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/TargetMap.java +++ b/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/TargetMap.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; /** * Mapping from source instances to target instances. Given that one source class can be the source for multiple @@ -16,15 +17,15 @@ public class TargetMap { private sealed interface Mapping { - Mapping with(EObject value); + Mapping join(Mapping other); } private record None() implements Mapping { @Override - public Mapping with(EObject value) { - return new One(value); + public Mapping join(Mapping other) { + return other; } } @@ -32,8 +33,11 @@ public Mapping with(EObject value) { private record One(EObject value) implements Mapping { @Override - public Mapping with(EObject value) { - return new Many(); + public Mapping join(Mapping other) { + return switch (other) { + case None ignored -> this; + default -> new Many(); + }; } } @@ -41,7 +45,7 @@ public Mapping with(EObject value) { private record Many() implements Mapping { @Override - public Mapping with(EObject value) { + public Mapping join(Mapping other) { return this; } @@ -56,6 +60,14 @@ private Mapping getMapping(EObject source, AQRTargetClass targetClass) { ); } + private Mapping getMappingInstanceOf(EObject source, AQRTargetClass targetClass) { + return map.entrySet().stream() + .filter(entry -> entry.getKey().left().equals(source)) + .filter(entry -> entry.getKey().right().equals(targetClass) || entry.getKey().right().allSuperClasses().contains(targetClass)) + .map(Entry::getValue) + .reduce(new None(), Mapping::join); + } + /** * Register a new mapping from the source instance to the target instance via the target class. */ @@ -63,7 +75,7 @@ public void register(EObject source, AQRTargetClass targetClass, EObject target) var previous = getMapping(source, targetClass); map.put( new Pair<>(source, targetClass), - previous.with(target) + previous.join(new One(target)) ); } @@ -73,14 +85,30 @@ public void register(EObject source, AQRTargetClass targetClass, EObject target) * @throws TransformatorException if none or multiple target instances are mapped to the given source instance and target class */ public EObject get(EObject source, AQRTargetClass targetClass) { - return switch (getMapping(source, targetClass)) { + return get(source, targetClass, false); + } + + /** + * Retrieve the target instance that is mapped to the given source instance and target class or subclasses or the target class. + * + * @throws TransformatorException if none or multiple target instances are mapped to the given source instance and target class + */ + public EObject get(EObject source, AQRTargetClass targetClass, boolean allowSubclasses) { + Mapping mapping; + if (allowSubclasses) { + mapping = getMappingInstanceOf(source, targetClass); + } else { + mapping = getMapping(source, targetClass); + } + + return switch (mapping) { case One(var value) -> value; case None ignored -> throw new TransformatorException( - "no target instance of class '%s' found for source instance of class '%s'".formatted( - targetClass.name(), source.eClass().getName() - ) - ); - case Many ignored -> throw new TransformatorException( + "no target instance of class '%s' found for source instance of class '%s'".formatted( + targetClass.name(), source.eClass().getName() + ) + ); + case Many ignored -> throw new TransformatorException( "multiple target instances of class '%s' found for source instance of class '%s'".formatted( targetClass.name(), source.eClass().getName() ) diff --git a/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/Transformator.java b/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/Transformator.java index 1dbd1af6..fd573d93 100644 --- a/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/Transformator.java +++ b/lang/backend-emf/src/main/java/tools/vitruv/neojoin/transformation/Transformator.java @@ -97,6 +97,7 @@ public EObject transform() throws TransformatorException { // create other instances aqr.classes().stream() .filter(target -> target != aqr.root()) + .filter(target -> !target.isAbstract()) .forEach(target -> { var instances = transformTargetClass(target); var rootRef = root.eClass() @@ -306,9 +307,9 @@ private void populateReference( private Object mapInstances(Object instance, AQRTargetClass target) { if (instance instanceof List list) { - return list.stream().map(i -> targetMap.get((EObject) i, target)).toList(); + return list.stream().map(i -> targetMap.get((EObject) i, target, true)).toList(); } else { - return targetMap.get((EObject) instance, target); + return targetMap.get((EObject) instance, target, true); } } @@ -342,6 +343,8 @@ private void checkNotAlreadyContained(EObject value, EObject target, AQRFeature. } else if (featureKind instanceof AQRFeature.Kind.Copy.Implicit(EStructuralFeature feature)) { check(source != null && source.eClass() == feature.getEContainingClass()); return source.eGet(feature); + } else if (featureKind instanceof AQRFeature.Kind.Override(AQRFeature overridden, AQRFeature.Kind overriding)) { + return evaluateFeature(overriding, source, context); } else { return fail(); } diff --git a/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/GenerateTest.java b/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/GenerateTest.java index a93ad6cf..a9b51884 100644 --- a/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/GenerateTest.java +++ b/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/GenerateTest.java @@ -21,7 +21,7 @@ public class GenerateTest { - static List validQueries = List.of("actor-rating", "books-on-tape", "customer-borrowings", "movies"); + static List validQueries = List.of("actor-rating", "books-on-tape", "customer-borrowings", "movies", "pizza2"); /** * {@code java -jar cli.jar --meta-model-path= --generate= } diff --git a/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/TransformTest.java b/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/TransformTest.java index 40ecf54e..e3ae23ed 100644 --- a/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/TransformTest.java +++ b/lang/frontend/cli/src/test/java/tools/vitruv/neojoin/cli/integration/TransformTest.java @@ -25,7 +25,7 @@ public class TransformTest { - static List validQueries = List.of("pizza"); + static List validQueries = List.of("pizza", "pizza2"); @BeforeAll public static void setupRegistry() { diff --git a/lang/frontend/cli/src/test/resources/models/pizza2.ecore b/lang/frontend/cli/src/test/resources/models/pizza2.ecore new file mode 100644 index 00000000..b42be8da --- /dev/null +++ b/lang/frontend/cli/src/test/resources/models/pizza2.ecore @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lang/frontend/cli/src/test/resources/queries/pizza2.nj b/lang/frontend/cli/src/test/resources/queries/pizza2.nj new file mode 100644 index 00000000..f41afa44 --- /dev/null +++ b/lang/frontend/cli/src/test/resources/queries/pizza2.nj @@ -0,0 +1,21 @@ +export pizza to "http://example.org/pizza2" + +import "http://example.org/restaurant" + +create abstract Restaurant { + name: EString + sells: Item [*] +} + +create Item { + name := "" +} +from Food +create extends Item + +from Restaurant +where name.startsWith("Pizzeria") +create Pizzeria extends Restaurant { + name := name + sells := sells +} diff --git a/lang/frontend/cli/src/test/resources/results/pizza2.ecore b/lang/frontend/cli/src/test/resources/results/pizza2.ecore new file mode 100644 index 00000000..b42be8da --- /dev/null +++ b/lang/frontend/cli/src/test/resources/results/pizza2.ecore @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lang/frontend/cli/src/test/resources/results/pizza2.xmi b/lang/frontend/cli/src/test/resources/results/pizza2.xmi new file mode 100644 index 00000000..36e84feb --- /dev/null +++ b/lang/frontend/cli/src/test/resources/results/pizza2.xmi @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/PlantUMLBuilder.java b/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/PlantUMLBuilder.java index 9bcd3bf1..bf080a2c 100644 --- a/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/PlantUMLBuilder.java +++ b/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/PlantUMLBuilder.java @@ -60,8 +60,8 @@ public void literal(String name) { appendln(name); } - public void clazz(String name, Runnable content) { - block("class " + name, content); + public void clazz(String name, boolean isAbstract, Runnable content) { + block((isAbstract ? "abstract " : "") + "class " + name, content); } public void attribute(String name, String type) { diff --git a/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/VisualizationGenerator.java b/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/VisualizationGenerator.java index fb734cfe..c8442478 100644 --- a/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/VisualizationGenerator.java +++ b/lang/frontend/ide/src/main/java/tools/vitruv/neojoin/ide/visualization/VisualizationGenerator.java @@ -128,7 +128,7 @@ public String generate() { .forEach(classifier -> { if (classifier instanceof EClass clazz) { // class might have already been generated as super class of another class - clazzIfNew(clazz); + clazzIfNew(clazz, false); } else if (classifier instanceof EEnum eEnum) { // enum might have already been generated from a class attribute enumerationIfNew(eEnum); @@ -141,14 +141,16 @@ public String generate() { // show selected target classes selected.selectedClasses.stream() .sorted(Comparator.comparing(EClassifier::getName)) - .forEach(this::targetClazz); + // class might have already been generated as super class of another class + .forEach(this::targetClazzIfNew); } else { // show all target classes targetMetaModel.pack().getEClassifiers().stream() .sorted(Comparator.comparing(EClassifier::getName)) .forEach(classifier -> { if (classifier instanceof EClass clazz) { - targetClazz(clazz); + // class might have already been generated as super class of another class + targetClazzIfNew(clazz); } else if (classifier instanceof EEnum eEnum) { // enum might have already been generated from a class attribute enumerationIfNew(eEnum); @@ -195,14 +197,20 @@ private void packages() { out.pack("\"Target: %s\" as %s".formatted(targetName, targetName), Empty); } + private void targetClazzIfNew(EClass target) { + if (!seenClazzes.contains(target)) { + targetClazz(target); + } + } + private void targetClazz(EClass target) { - clazz(target); + clazz(target, true); // show arrow from source to target classes var sourceClasses = targetMetaModel.trace().getSourceClassesForTargetClass(target); if (sourceClasses != null) { sourceClasses.forEach(source -> { - clazzIfNew(source); + clazzIfNew(source, true); out.arrow( getQualifiedName(source), ArrowSourceClass, @@ -214,13 +222,13 @@ private void targetClazz(EClass target) { private final HashSet seenClazzes = new HashSet<>(); - private void clazzIfNew(EClass clazz) { + private void clazzIfNew(EClass clazz, boolean target) { if (!seenClazzes.contains(clazz)) { - clazz(clazz); + clazz(clazz, target); } } - private void clazz(EClass clazz) { + private void clazz(EClass clazz, boolean target) { var isNew = seenClazzes.add(clazz); check(isNew); @@ -228,7 +236,7 @@ private void clazz(EClass clazz) { // class with attributes out.clazz( - qualifiedName, () -> { + qualifiedName, clazz.isAbstract(), () -> { clazz.getEAttributes().forEach(attr -> { var type = getQualifiedName(attr.getEType()); out.attribute(attr.getName(), type); @@ -249,7 +257,11 @@ private void clazz(EClass clazz) { // super types for (var superType : clazz.getESuperTypes()) { - clazzIfNew(superType); + if (target) { + targetClazzIfNew(superType); + } else { + clazzIfNew(superType, false); + } var superName = getQualifiedName(superType); out.arrow(qualifiedName, PlantUMLBuilder.ReferenceInheritanceUpwards, superName); } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/aqr/AQRBuilder.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/aqr/AQRBuilder.java index d17866ee..0e394948 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/aqr/AQRBuilder.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/aqr/AQRBuilder.java @@ -13,7 +13,11 @@ import org.eclipse.xtext.xbase.XExpression; import org.jspecify.annotations.Nullable; import tools.vitruv.neojoin.Constants; +import tools.vitruv.neojoin.ast.AbstractFeature; +import tools.vitruv.neojoin.ast.AbstractMainQuery; import tools.vitruv.neojoin.ast.Body; +import tools.vitruv.neojoin.ast.ConcreteFeature; +import tools.vitruv.neojoin.ast.ConcreteMainQuery; import tools.vitruv.neojoin.ast.Export; import tools.vitruv.neojoin.ast.Feature; import tools.vitruv.neojoin.ast.Import; @@ -64,7 +68,7 @@ public class AQRBuilder { private final ViewTypeDefinition viewTypeDefinition; private final ExpressionHelper expressionHelper; - private final Queue> populationQueue = new ArrayDeque<>(); // target class + query it originated from (or null if implicit) + private final Queue> populationQueue = new ArrayDeque<>(); // target class + query it originated from (or null if implicit) private final Set encounteredDataTypes = new HashSet<>(); @@ -90,10 +94,21 @@ public AQR build() { // create empty (= without features) target classes for all queries AstUtils.getAllQueries(viewTypeDefinition).forEach(this::createTargetClass); + // add super classes to target classes + for (var entry : populationQueue) { + addSuperClassesToTargetClass(entry.left(), entry.right()); + } + + // populate target classes while (!populationQueue.isEmpty()) { var entry = populationQueue.poll(); //noinspection DataFlowIssue - false positive - populateTargetClass(entry.left(), entry.right()); + populateTargetClass(entry.left(), entry.right() == null ? null : AstUtils.getBody(entry.right())); + } + + // identify and verify inheritance + for (var targetClass : targetClasses) { + applyInheritance(targetClass); } var root = createRootIfNeededAndInit(); @@ -127,8 +142,8 @@ private AQRTargetClass getTargetForQuery(Query query) { return target; } - private AQRTargetClass createTargetClass(String name, @Nullable AQRSource source, @Nullable Query query) { - var target = new AQRTargetClass(name, source, new ArrayList<>()); + private AQRTargetClass createTargetClass(String name, boolean isAbstract, @Nullable AQRSource source, @Nullable Query query) { + var target = new AQRTargetClass(name, isAbstract, source, new ArrayList<>(), new ArrayList<>()); targetClasses.add(target); @@ -143,22 +158,31 @@ private AQRTargetClass createTargetClass(String name, @Nullable AQRSource source }); } - populationQueue.add(new Pair<>(target, query != null ? query.getBody() : null)); + populationQueue.add(new Pair<>(target, query)); return target; } private void createTargetClass(Query query) { - if (query instanceof MainQuery mainQuery) { + if (query instanceof ConcreteMainQuery mainQuery) { createTargetClass( AstUtils.getTargetName(mainQuery), + false, mainQuery.getSource() != null ? AQRSourceBuilder.createSource(mainQuery.getSource()) : null, mainQuery ); + } else if (query instanceof AbstractMainQuery mainQuery) { + createTargetClass( + AstUtils.getTargetName(mainQuery), + true, + null, + mainQuery + ); } else if (query instanceof SubQuery subQuery) { var sourceType = AstUtils.inferSubQuerySourceType(subQuery, expressionHelper); invariant(sourceType != null); createTargetClass( AstUtils.getTargetName(subQuery, sourceType), + false, AQRSourceBuilder.createSource(sourceType), subQuery ); @@ -177,7 +201,7 @@ private void createTargetClass(Query query) { private AQRTargetClass getOrCreateTargetClass(EClass source) { var targets = sourceClassToAQR.get(source); if (targets == null) { - return createTargetClass(source.getName(), AQRSourceBuilder.createSource(source), null); + return createTargetClass(source.getName(), source.isAbstract(), AQRSourceBuilder.createSource(source), null); } invariant( @@ -191,6 +215,16 @@ private AQRTargetClass getOrCreateTargetClass(EClass source) { return targets.iterator().next(); } + private void addSuperClassesToTargetClass(AQRTargetClass targetClazz, @Nullable Query query) { + if (query != null && query instanceof MainQuery mainQuery) { + for (EObject superClass : mainQuery.getSuperClasses()) { + invariant(superClass instanceof Query, "Classes can only extend classes created from other queries"); + + targetClazz.superClasses().add(getTargetForQuery((Query) superClass)); + } + } + } + /** * Populates the target class with features specified in the given body or copies all features if no body was given. * @@ -199,7 +233,7 @@ private AQRTargetClass getOrCreateTargetClass(EClass source) { */ private void populateTargetClass(AQRTargetClass targetClazz, @Nullable Body body) { if (body != null) { // create features based on definition in Body - for (Feature feature : body.getFeatures()) { + for (Feature feature : AstUtils.getFeatures(body)) { targetClazz.features().add(createFeature(feature)); } } else { // copy all features from the source class @@ -238,16 +272,26 @@ private AQRFeature createFeature(Feature feature) { var kind = getFeatureKind(feature); var name = getFeatureName(feature, kind); - var inferredType = inferType(feature.getExpression()); - AQRFeature.Options options = AQRFeatureOptionsBuilder.build(feature, kind.source(), inferredType.isMany()); + TypeInfo inferredType = null; + AQRFeature.Options options; + SubQuery subQuery = null; + if (feature instanceof ConcreteFeature concreteFeature) { + inferredType = inferType(concreteFeature.getExpression()); + options = AQRFeatureOptionsBuilder.build(feature, kind.source(), inferredType.isMany()); + subQuery = concreteFeature.getSubQuery(); + } else if (feature instanceof AbstractFeature) { + options = AQRFeatureOptionsBuilder.build(feature, null, AstUtils.isManyMultiplicityDefinition(feature)); + } else { + return fail("Feature must be either a concrete or an abstract feature"); + } if (isAttribute(inferredType, feature.getType())) { - invariant(feature.getSubQuery() == null); + invariant(subQuery == null); var type = determineAttributeType(inferredType, (EDataType) feature.getType(), kind); encounteredDataTypes.add(type); return new AQRFeature.Attribute(name, type, kind, options); } else { - var type = determineReferenceType(inferredType, (Query) feature.getType(), feature.getSubQuery()); + var type = determineReferenceType(inferredType, (Query) feature.getType(), subQuery); return new AQRFeature.Reference(name, type, kind, options); } } @@ -256,21 +300,27 @@ private AQRFeature createFeature(Feature feature) { * Determines the {@link AQRFeature.Kind kind} of the feature. */ private AQRFeature.Kind getFeatureKind(Feature feature) { - return switch (feature.getOp()) { - case COPY -> { - try { - var type = expressionHelper.getFeatureOrNull(feature.getExpression()); - invariant( - type != null, - () -> "Copy feature expression must reference a feature: " + feature.getExpression() - ); - yield new AQRFeature.Kind.Copy.Explicit(type, feature.getExpression()); - } catch (TypeResolutionException e) { - yield invariantFailed(); // should have been handled by Xbase type checking + if (feature instanceof ConcreteFeature concreteFeature) { + return switch (concreteFeature.getOp()) { + case COPY -> { + try { + var type = expressionHelper.getFeatureOrNull(concreteFeature.getExpression()); + invariant( + type != null, + () -> "Copy feature expression must reference a feature: " + concreteFeature.getExpression() + ); + yield new AQRFeature.Kind.Copy.Explicit(type, concreteFeature.getExpression()); + } catch (TypeResolutionException e) { + yield invariantFailed(); // should have been handled by Xbase type checking + } } - } - case CALCULATE -> new AQRFeature.Kind.Calculate(feature.getExpression()); - }; + case CALCULATE -> new AQRFeature.Kind.Calculate(concreteFeature.getExpression()); + }; + } else if (feature instanceof AbstractFeature) { + return new AQRFeature.Kind.Abstract(); + } else { + return fail("Feature must be either a concrete or an abstract feature"); + } } /** @@ -284,7 +334,9 @@ private static String getFeatureName(Feature feature, AQRFeature.Kind kind) { if (kind instanceof AQRFeature.Kind.Copy copy) { return copy.source().getName(); } else if (kind instanceof AQRFeature.Kind.Calculate) { - return invariantFailed("Calculated feature must have a name: " + feature.getExpression()); + return invariantFailed("Calculated feature must have a name: " + ((ConcreteFeature) feature).getExpression()); + } else if (kind instanceof AQRFeature.Kind.Override overriding) { + return overriding.overridden().name(); } else { return fail(); } @@ -295,11 +347,11 @@ private static String getFeatureName(Feature feature, AQRFeature.Kind kind) { * * @return {@code true} if the feature is an attribute, {@code false} if it is a reference */ - private boolean isAttribute(TypeInfo inferredType, @Nullable EObject explicitType) { + private boolean isAttribute(@Nullable TypeInfo inferredType, @Nullable EObject explicitType) { if (explicitType != null) { return explicitType instanceof EDataType; } else { - invariant(inferredType.classifier() != null); + invariant(inferredType != null && inferredType.classifier() != null); return inferredType.classifier() instanceof EDataType; } } @@ -308,15 +360,15 @@ private boolean isAttribute(TypeInfo inferredType, @Nullable EObject explicitTyp * Determine the {@link EAttribute#getEAttributeType() type} of the attribute. */ private EDataType determineAttributeType( - TypeInfo inferredType, + @Nullable TypeInfo inferredType, @Nullable EDataType explicitType, AQRFeature.Kind kind ) { if (explicitType != null) { // explicit type - invariant(inferredType.classifier() == null || isAssignable(explicitType, inferredType.classifier())); + invariant(inferredType == null || inferredType.classifier() == null || isAssignable(explicitType, inferredType.classifier())); return explicitType; } else { // implicit type - invariant(inferredType.classifier() != null); + invariant(inferredType != null && inferredType.classifier() != null); if (kind instanceof AQRFeature.Kind.Copy copy) { var type = (EDataType) copy.source().getEType(); invariant(isAssignable(type, inferredType.classifier())); @@ -339,21 +391,25 @@ private static boolean isAssignable(EDataType to, EClassifier from) { * Determine the {@link EReference#getEReferenceType() type} of the reference. */ private AQRTargetClass determineReferenceType( - TypeInfo inferredType, + @Nullable TypeInfo inferredType, @Nullable Query explicitType, @Nullable SubQuery subQuery ) { if (explicitType != null) { + var explicitTargetType = getTargetForQuery(explicitType); + invariant(subQuery == null || explicitType == subQuery); - if (inferredType.classifier() != null) { - var inferredClass = (EClass) inferredType.classifier(); - - // check that the inferred type is assignable to the explicit type - if (explicitType instanceof MainQuery explicitMainQueryType) { - invariant(explicitMainQueryType.getSource() != null, "Cannot reference a query without source"); - invariant(AstUtils.checkSourceType(explicitMainQueryType.getSource(), inferredClass)); - } else { - var explicitSubQueryType = (SubQuery) explicitType; + if (inferredType != null && inferredType.classifier() instanceof EClass inferredClass) { + // check that the inferred type is unambiguously assignable to the explicit type + var inferredTargetClasses = sourceClassToAQR.get(inferredClass); + var assignableTargetClasses = inferredTargetClasses.stream() + .filter(targetClass -> + targetClass.equals(explicitTargetType) || + targetClass.allSuperClasses().contains(explicitTargetType)) + .toList(); + invariant(assignableTargetClasses.size() == 1, "Expression cannot be assigned to feature with this type"); + + if (explicitType instanceof SubQuery explicitSubQueryType) { var subQuerySourceType = AstUtils.inferSubQuerySourceType( explicitSubQueryType, expressionHelper @@ -362,9 +418,9 @@ private AQRTargetClass determineReferenceType( invariant(AstUtils.checkSourceType(subQuerySourceType, inferredClass)); } } - return getTargetForQuery(explicitType); + return explicitTargetType; } else { - invariant(inferredType.classifier() instanceof EClass); // includes null check + invariant(inferredType != null && inferredType.classifier() instanceof EClass); // includes null check if (subQuery != null) { return getTargetForQuery(subQuery); } else { @@ -381,6 +437,45 @@ private TypeInfo inferType(XExpression expression) { } } + private void applyInheritance(AQRTargetClass targetClass) { + var superClassFeatures = targetClass.superClasses().stream().flatMap(superClass -> superClass.features().stream()).toList(); + + // update overriding features + targetClass.features().replaceAll(feature -> { + var overriddenFeatures = superClassFeatures.stream().filter(superClassFeature -> superClassFeature.name().equals(feature.name())).toList(); + if (overriddenFeatures.isEmpty()) { + return feature; + } + + invariant(overriddenFeatures.size() == 1); + var overriddenFeature = overriddenFeatures.get(0); + + if (feature instanceof AQRFeature.Attribute attribute) { + invariant(overriddenFeature instanceof AQRFeature.Attribute, "Attribute must override attribute"); + var overriddenAttribute = (AQRFeature.Attribute)overriddenFeature; + invariant(overriddenAttribute.type().equals(attribute.type()), "Type of overriding feature must be equal to type of overridden feature"); + + return attribute + .withFeatureKind(new AQRFeature.Kind.Override(overriddenFeature, feature.kind())) + .withOptions(overriddenFeature.options()); + } else if (feature instanceof AQRFeature.Reference reference) { + invariant(overriddenFeature instanceof AQRFeature.Reference, "Reference must override reference"); + var overriddenReference = (AQRFeature.Reference)overriddenFeature; + invariant(overriddenReference.type().equals(reference.type()) || reference.type().allSuperClasses().contains(overriddenReference.type()), "Type of overriding feature must be equal to or a subtype of the type of the overridden feature"); + + return reference + .withFeatureKind(new AQRFeature.Kind.Override(overriddenFeature, feature.kind())) + .withOptions(overriddenFeature.options()); + } else { + throw new IllegalStateException("AQRFeature is a sealed interface therefore this check should be exhaustive"); + } + }); + + // check if all inherited features are overridden + invariant(superClassFeatures.stream().allMatch(superClassFeature -> targetClass.features().stream().anyMatch(feature -> feature.name().equals(superClassFeature.name()) && (feature.kind() instanceof AQRFeature.Kind.Override))), + "Sub classes must override all inherited features"); + } + private AQRTargetClass createRootIfNeededAndInit() { AQRTargetClass root; @@ -388,7 +483,7 @@ private AQRTargetClass createRootIfNeededAndInit() { if (rootQuery != null) { root = getTargetForQuery(rootQuery); } else { - root = new AQRTargetClass(Constants.DefaultRootClassName, null, new ArrayList<>()); + root = new AQRTargetClass(Constants.DefaultRootClassName, false, null, new ArrayList<>(), new ArrayList<>()); targetClasses.add(root); } @@ -402,7 +497,7 @@ private AQRTargetClass createRootIfNeededAndInit() { * @return the root query if it exists, {@code null} otherwise */ private @Nullable MainQuery findRootQuery() { - var roots = viewTypeDefinition.getQueries().stream().filter(MainQuery::isRoot).toList(); + var roots = viewTypeDefinition.getQueries().stream().filter(query -> query instanceof ConcreteMainQuery mainQuery && mainQuery.isRoot()).toList(); invariant( roots.size() <= 1, () -> "Multiple root queries found: " + roots.stream() @@ -434,7 +529,7 @@ private AQRTargetClass createRootIfNeededAndInit() { */ private void populateRoot(AQRTargetClass root) { for (var target : targetClasses) { - if (target == root) { + if (target == root || target.isAbstract()) { continue; } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/generation/MetaModelGenerator.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/generation/MetaModelGenerator.java index c0782565..9cf22a3e 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/generation/MetaModelGenerator.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/generation/MetaModelGenerator.java @@ -93,6 +93,7 @@ protected EPackage createPackage() { protected EClass createClass(AQRTargetClass targetClass) { EClass target = Ecore.createEClass(); target.setName(targetClass.name()); + target.setAbstract(targetClass.isAbstract()); trace.targetToAqr().put(target, targetClass); trace.aqrToTarget().put(targetClass, target); @@ -105,7 +106,11 @@ protected EClass createClass(AQRTargetClass targetClass) { private void populateClass(AQRTargetClass targetClass) { var target = Objects.requireNonNull(trace.aqrToTarget().get(targetClass)); - var features = targetClass.features().stream().map(this::createFeature).toList(); + + var superTypes = targetClass.superClasses().stream().map(superClass -> trace.aqrToTarget().get(superClass)).toList(); + target.getESuperTypes().addAll(superTypes); + + var features = targetClass.features().stream().filter(feature -> !(feature.kind() instanceof AQRFeature.Kind.Override)).map(this::createFeature).toList(); target.getEStructuralFeatures().addAll(features); } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/jvmmodel/QueryModelInferrer.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/jvmmodel/QueryModelInferrer.java index ae8489e0..4a8b878f 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/jvmmodel/QueryModelInferrer.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/jvmmodel/QueryModelInferrer.java @@ -1,8 +1,6 @@ package tools.vitruv.neojoin.jvmmodel; import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.EDataType; -import org.eclipse.emf.ecore.EEnum; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.common.types.JvmGenericType; import org.eclipse.xtext.common.types.JvmOperation; @@ -17,7 +15,8 @@ import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder; import org.jspecify.annotations.Nullable; import tools.vitruv.neojoin.Constants; -import tools.vitruv.neojoin.ast.Body; +import tools.vitruv.neojoin.ast.ConcreteBody; +import tools.vitruv.neojoin.ast.ConcreteMainQuery; import tools.vitruv.neojoin.ast.From; import tools.vitruv.neojoin.ast.MainQuery; import tools.vitruv.neojoin.ast.Source; @@ -77,14 +76,16 @@ public void infer() { viewType.eResource().getContents().add(root); // otherwise type resolution fails for (MainQuery q : viewType.getQueries()) { - inferMainQuery(q); + if (q instanceof ConcreteMainQuery c) { + inferConcreteMainQuery(c); + } } viewType.eResource().getContents().remove(root); acceptor.accept(root); } - private void inferMainQuery(MainQuery mainQuery) { + private void inferConcreteMainQuery(ConcreteMainQuery mainQuery) { var targetName = AstUtils.getTargetName(mainQuery); if (mainQuery.getSource() != null) { @@ -128,7 +129,7 @@ private void inferMainQuery(MainQuery mainQuery) { } if (mainQuery.getBody() != null) { - inferBody( + inferConcreteBody( mainQuery.getBody(), targetName, paramsForSource(mainQuery.getSource(), AstUtils.isGrouping(mainQuery.getSource()), null) @@ -136,7 +137,7 @@ private void inferMainQuery(MainQuery mainQuery) { } } - private void inferBody(Body body, String name, Consumer addParams) { + private void inferConcreteBody(ConcreteBody body, String name, Consumer addParams) { Utils.forEachIndexed( body.getFeatures(), (feature, index) -> { var exprName = name + "_feature_" + index; @@ -147,7 +148,7 @@ private void inferBody(Body body, String name, Consumer addParams) var featureType = inferEClassFromExpressionOrNull(feature.getExpression()); if (featureType != null) { var subQueryName = AstUtils.getTargetName(feature.getSubQuery(), featureType); - inferBody( + inferConcreteBody( feature.getSubQuery().getBody(), subQueryName, paramsForClass(featureType, feature.getSubQuery()) diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/scoping/NeoJoinScopeProvider.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/scoping/NeoJoinScopeProvider.java index f1d99dc5..269e339d 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/scoping/NeoJoinScopeProvider.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/scoping/NeoJoinScopeProvider.java @@ -63,6 +63,8 @@ public IScope getScope(EObject context, EReference reference) { var from = join.getFrom(); if (source != null && from != null) { return createJoinConditionOtherScope(source, from); + } else { + return IScope.NULLSCOPE; } } else if (reference == AstPackage.Literals.JOIN_FEATURE_CONDITION__FEATURES) { var condition = (JoinFeatureCondition) context; @@ -77,14 +79,16 @@ public IScope getScope(EObject context, EReference reference) { var right = join.getFrom().getClazz(); if (left != null && right != null) { return createJoinConditionFieldsScope(left, right); + } else { + return IScope.NULLSCOPE; } + } else if (reference == AstPackage.Literals.MAIN_QUERY__SUPER_CLASSES) { + return createSuperClassScope(AstUtils.getViewType(context)); } else if (reference == AstPackage.Literals.FEATURE__TYPE) { return createFeatureTypeScope(AstUtils.getViewType(context)); } else { return super.getScope(context, reference); } - - return IScope.NULLSCOPE; } /** @@ -137,6 +141,14 @@ private IScope createJoinConditionFieldsScope(EClass left, EClass right) { return new SimpleScope(IScope.NULLSCOPE, candidates); } + private IScope createSuperClassScope(ViewTypeDefinition viewType) { + var queryCandidates = AstUtils.getAllQueries(viewType) + .map(query -> EObjectDescription.create(AstUtils.getTargetName(query, expressionHelper), query)) + .toList(); + + return new SimpleScope(queryCandidates); + } + /** * Scope for available types when specifying an explicit type for a feature. */ diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/utils/AstUtils.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/utils/AstUtils.java index 4a4cf29f..53b4986e 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/utils/AstUtils.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/utils/AstUtils.java @@ -7,6 +7,12 @@ import org.eclipse.xtext.EcoreUtil2; import org.jspecify.annotations.Nullable; import tools.vitruv.neojoin.aqr.AQRFeatureOptionsBuilder; +import tools.vitruv.neojoin.ast.AbstractBody; +import tools.vitruv.neojoin.ast.AbstractMainQuery; +import tools.vitruv.neojoin.ast.Body; +import tools.vitruv.neojoin.ast.ConcreteBody; +import tools.vitruv.neojoin.ast.ConcreteFeature; +import tools.vitruv.neojoin.ast.ConcreteMainQuery; import tools.vitruv.neojoin.ast.Feature; import tools.vitruv.neojoin.ast.From; import tools.vitruv.neojoin.ast.Import; @@ -20,6 +26,7 @@ import tools.vitruv.neojoin.jvmmodel.ExpressionHelper; import tools.vitruv.neojoin.jvmmodel.TypeResolutionException; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Stream; @@ -43,10 +50,12 @@ public static String getTargetName(MainQuery mainQuery) { return mainQuery.getName(); } - if (mainQuery.getSource() != null) { - var source = mainQuery.getSource().getFrom().getClazz(); - if (source != null && source.getName() != null) { - return source.getName(); + if (mainQuery instanceof ConcreteMainQuery concreteMainQuery) { + if (concreteMainQuery.getSource() != null) { + var source = concreteMainQuery.getSource().getFrom().getClazz(); + if (source != null && source.getName() != null) { + return source.getName(); + } } } @@ -101,7 +110,7 @@ public static String getTargetName(Query query, ExpressionHelper expressionHelpe SubQuery subQuery, ExpressionHelper expressionHelper ) { - if (subQuery.eContainer() instanceof Feature feature) { + if (subQuery.eContainer() instanceof ConcreteFeature feature) { try { var typeInfo = expressionHelper.inferEType(feature.getExpression()); if (typeInfo != null && typeInfo.classifier() instanceof EClass clazz) { @@ -174,6 +183,27 @@ public static boolean checkSourceType(Source source, EClass inferred) { return getAllFroms(source).anyMatch(f -> checkSourceType(f.getClazz(), inferred)); } + /** + * Check whether the given query defines a super type of the type defined by the given other query. + * + * @return {@code true} if the given query is a super type of the given other query + */ + public static boolean isSuperTypeOf(Query superType, Query subType) { + if (subType instanceof MainQuery subMainQuery) { + if (subMainQuery.getSuperClasses().contains(superType)) { + return true; + } + for (var subSuperType : subMainQuery.getSuperClasses()) { + if (subSuperType instanceof Query subSuperQuery) { + if (isSuperTypeOf(superType, subSuperQuery)) { + return true; + } + } + } + } + return false; + } + /** * Get all {@link From froms} contained in {@code source} including the one {@link Source#getFrom() directly contained} as well as those contained in {@link Source#getJoins() joins}. * @@ -227,6 +257,15 @@ public static boolean isManyMultiplicity(MultiplicityExpr multiplicity) { return isManyMultiplicity(AQRFeatureOptionsBuilder.normalizeMultiplicity(multiplicity)); } + public static boolean isManyMultiplicityDefinition(Feature feature) { + var explicitMultiplicity = AstUtils.findMultiplicityExpression(feature); + if (explicitMultiplicity != null) { + return AstUtils.isManyMultiplicity(explicitMultiplicity); + } else { + return false; // should we try to infer this from the declared type? + } + } + /** * Get all queries (main and sub queries) contained in the given view type. */ @@ -236,14 +275,42 @@ public static Stream getAllQueries(ViewTypeDefinition viewType) { } private static Stream getAllQueriesImpl(Query query) { - if (query.getBody() == null) { + var body = getBody(query); + if (body == null) { return Stream.of(query); - } else { - var subQueries = query.getBody().getFeatures().stream() - .map(Feature::getSubQuery) + } else if (body instanceof ConcreteBody concreteBody) { + var subQueries = concreteBody.getFeatures().stream() + .map(ConcreteFeature::getSubQuery) .filter(Objects::nonNull) .flatMap(AstUtils::getAllQueriesImpl); return Stream.concat(Stream.of(query), subQueries); + } else if (body instanceof AbstractBody) { + // abstract features cannot have sub queries + return Stream.of(query); + } else { + return fail("A query body must be either an abstract or a concrete body"); + } + } + + public static Body getBody(Query query) { + if (query instanceof ConcreteMainQuery concreteMainQuery) { + return concreteMainQuery.getBody(); + } else if (query instanceof AbstractMainQuery abstractMainQuery) { + return abstractMainQuery.getBody(); + } else if (query instanceof SubQuery subQuery) { + return subQuery.getBody(); + } else { + return fail("Query must be either a concrete main query, an abstract main query, or a sub query"); + } + } + + public static List getFeatures(Body body) { + if (body instanceof ConcreteBody concreteBody) { + return concreteBody.getFeatures().stream().map(e -> (Feature) e).toList(); + } else if (body instanceof AbstractBody abstractBody) { + return abstractBody.getFeatures().stream().map(e -> (Feature) e).toList(); + } else { + return fail("Body must be either a concrete query body or an abstract query body"); } } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureModifierValidator.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureModifierValidator.java index 89ca5d68..1f740dd6 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureModifierValidator.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureModifierValidator.java @@ -6,6 +6,7 @@ import org.jspecify.annotations.Nullable; import tools.vitruv.neojoin.ast.AstPackage; import tools.vitruv.neojoin.ast.BooleanModifier; +import tools.vitruv.neojoin.ast.ConcreteFeature; import tools.vitruv.neojoin.ast.Feature; import tools.vitruv.neojoin.ast.Modifier; import tools.vitruv.neojoin.ast.MultiplicityBounds; @@ -83,9 +84,9 @@ void checkApplicability(Feature feature) { private @Nullable Boolean isAttribute(Feature feature) { if (feature.getType() != null) { return feature.getType() instanceof EDataType; - } else { + } else if (feature instanceof ConcreteFeature concreteFeature) { try { - var inferredType = expressionHelper.inferEType(feature.getExpression()); + var inferredType = expressionHelper.inferEType(concreteFeature.getExpression()); if (inferredType != null) { return inferredType.classifier() instanceof EDataType; } @@ -145,26 +146,28 @@ void checkMultiplicity(Feature feature) { } var explicitIsMany = AstUtils.isManyMultiplicity(explicitMultiplicity); - try { - var inferredType = expressionHelper.inferEType(feature.getExpression()); - if (inferredType != null) { - var inferredIsMany = inferredType.isMany(); - if (explicitIsMany && !inferredIsMany) { - error( - "Cannot assign a single value to a multi-valued feature", - feature, - AstPackage.Literals.FEATURE__EXPRESSION - ); - } else if (!explicitIsMany && inferredIsMany) { - error( - "Cannot assign multiple values to a single-valued feature", - feature, - AstPackage.Literals.FEATURE__EXPRESSION - ); + if (feature instanceof ConcreteFeature concreteFeature) { + try { + var inferredType = expressionHelper.inferEType(concreteFeature.getExpression()); + if (inferredType != null) { + var inferredIsMany = inferredType.isMany(); + if (explicitIsMany && !inferredIsMany) { + error( + "Cannot assign a single value to a multi-valued feature", + concreteFeature, + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION + ); + } else if (!explicitIsMany && inferredIsMany) { + error( + "Cannot assign multiple values to a single-valued feature", + concreteFeature, + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION + ); + } } + } catch (TypeResolutionException e) { + // ignore: will be handled by type checking } - } catch (TypeResolutionException e) { - // ignore: will be handled by type checking } } } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureValidator.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureValidator.java index 13cd65af..dc922305 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureValidator.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/FeatureValidator.java @@ -15,9 +15,11 @@ import tools.vitruv.neojoin.Constants; import tools.vitruv.neojoin.aqr.AQRFeatureOptionsBuilder; import tools.vitruv.neojoin.ast.AstPackage; +import tools.vitruv.neojoin.ast.ConcreteBody; +import tools.vitruv.neojoin.ast.ConcreteFeature; +import tools.vitruv.neojoin.ast.ConcreteMainQuery; import tools.vitruv.neojoin.ast.Feature; import tools.vitruv.neojoin.ast.FeatureOp; -import tools.vitruv.neojoin.ast.MainQuery; import tools.vitruv.neojoin.ast.Query; import tools.vitruv.neojoin.ast.SubQuery; import tools.vitruv.neojoin.ast.ViewTypeDefinition; @@ -45,13 +47,15 @@ public class FeatureValidator extends ComposableValidator { return feature.getName(); } - try { - var eFeature = expressionHelper.getFeatureOrNull(feature.getExpression()); - if (eFeature != null) { - return eFeature.getName(); - } // else -> will be handled in checkCopyFeatureExpression - } catch (TypeResolutionException e) { - // ignore: will be handled by type checking + if (feature instanceof ConcreteFeature concreteFeature) { + try { + var eFeature = expressionHelper.getFeatureOrNull(concreteFeature.getExpression()); + if (eFeature != null) { + return eFeature.getName(); + } // else -> will be handled in checkCopyFeatureExpression + } catch (TypeResolutionException e) { + // ignore: will be handled by type checking + } } return null; @@ -59,11 +63,17 @@ public class FeatureValidator extends ComposableValidator { @Check public void checkUniqueFeatureNames(Query query) { - if (query.getBody() == null || query.getBody().getFeatures().isEmpty()) { + var body = AstUtils.getBody(query); + if (body == null) { + return; + } + + var features = AstUtils.getFeatures(body); + if (features.isEmpty()) { return; } - var groupedFeatures = query.getBody().getFeatures().stream() + var groupedFeatures = features.stream() .flatMap(feature -> { var name = getFeatureNameOrNull(feature); if (name != null) { @@ -90,7 +100,7 @@ public void checkUniqueFeatureNames(Query query) { } @Check - public void checkCopyFeatureExpression(Feature feature) { + public void checkCopyFeatureExpression(ConcreteFeature feature) { if (feature.getOp() == FeatureOp.COPY) { try { var eFeature = expressionHelper.getFeatureOrNull(feature.getExpression()); @@ -98,7 +108,7 @@ public void checkCopyFeatureExpression(Feature feature) { error( "Copy feature expression does not reference a feature", feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } catch (TypeResolutionException e) { @@ -115,7 +125,7 @@ private LightweightTypeReference getActualType(XExpression expression) { } @Check - public void checkFeatureType(Feature feature) { + public void checkFeatureType(ConcreteFeature feature) { TypeInfo inferredType; try { inferredType = expressionHelper.inferEType(feature.getExpression()); @@ -126,7 +136,7 @@ public void checkFeatureType(Feature feature) { error( "Unsupported type: " + getActualType(feature.getExpression()), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); return; } @@ -144,7 +154,7 @@ public void checkFeatureType(Feature feature) { error( "Cannot use a subquery with an attribute expression (%s)".formatted(dataType.getName()), feature, - AstPackage.Literals.FEATURE__SUB_QUERY + AstPackage.Literals.CONCRETE_FEATURE__SUB_QUERY ); } @@ -157,7 +167,7 @@ public void checkFeatureType(Feature feature) { inferredType.classifier().getName(), eEnum.getName() ), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } else if (feature.getType() instanceof EDataType eDataType) { @@ -169,7 +179,7 @@ public void checkFeatureType(Feature feature) { eDataType.getName(), eDataType.getInstanceClass().getSimpleName() ), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } else { @@ -178,27 +188,27 @@ public void checkFeatureType(Feature feature) { inferredType.classifier().getName(), eDataType.getName() ), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } } - private void checkNullExpression(Feature feature) { + private void checkNullExpression(ConcreteFeature feature) { if (feature.getType() == null) { // no explicit type - error("Cannot infer type", feature, AstPackage.Literals.FEATURE__EXPRESSION); + error("Cannot infer type", feature, AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION); } if (feature.getSubQuery() != null) { - error("Cannot use null expression for a subquery", feature, AstPackage.Literals.FEATURE__EXPRESSION); + error("Cannot use null expression for a subquery", feature, AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION); } } - private void checkSubQuery(Feature feature, @Nullable EClassifier inferredClassifier) { + private void checkSubQuery(ConcreteFeature feature, @Nullable EClassifier inferredClassifier) { if (inferredClassifier instanceof EDataType dataType) { error( "Cannot use a subquery with an attribute expression (%s)".formatted(dataType.getName()), feature, - AstPackage.Literals.FEATURE__SUB_QUERY + AstPackage.Literals.CONCRETE_FEATURE__SUB_QUERY ); } if (feature.getType() != null && feature.getType() != feature.getSubQuery()) { @@ -210,16 +220,18 @@ private void checkSubQuery(Feature feature, @Nullable EClassifier inferredClassi } } - private void checkQueryType(Feature feature, Query explicitType, EClassifier inferredClassifier) { - if (explicitType instanceof MainQuery mainQuery) { - if (mainQuery.getSource() != null && inferredClassifier instanceof EClass inferredClass) { - if (AstUtils.checkSourceType(mainQuery.getSource(), inferredClass)) { - return; - } + private void checkQueryType(ConcreteFeature feature, Query explicitType, EClassifier inferredClassifier) { + if (inferredClassifier instanceof EClass inferredClass) { + var sourceMap = createSourceMap(AstUtils.getViewType(feature)); + var inferredType = sourceMap.get(inferredClass); + if (inferredType.stream().anyMatch(type -> type.equals(explicitType))) { + return; + } else if (inferredType.stream().anyMatch(type -> AstUtils.isSuperTypeOf(explicitType, type))) { + return; } - } else { - var subQuery = (SubQuery) explicitType; + } + if (explicitType instanceof SubQuery subQuery) { if (inferredClassifier instanceof EClass inferredClass) { var subQuerySourceType = AstUtils.inferSubQuerySourceType(subQuery, expressionHelper); if (subQuerySourceType == null) { @@ -236,18 +248,18 @@ private void checkQueryType(Feature feature, Query explicitType, EClassifier inf inferredClassifier.getName(), AstUtils.getTargetName(explicitType, expressionHelper) ), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } @Check - public void checkFeatureNullability(Feature feature) { + public void checkFeatureNullability(ConcreteFeature feature) { if (feature.getExpression() instanceof XMemberFeatureCall featureCall) { if (featureCall.isNullSafe() && isRequired(feature)) { warning( "Nullable expression used to initialize non-nullable feature", feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } @@ -255,9 +267,9 @@ public void checkFeatureNullability(Feature feature) { private boolean isRequired(Feature feature) { EStructuralFeature copyFrom = null; - if (feature.getOp() == FeatureOp.COPY) { + if (feature instanceof ConcreteFeature concreteFeature && concreteFeature.getOp() == FeatureOp.COPY) { try { - copyFrom = expressionHelper.getFeatureOrNull(feature.getExpression()); + copyFrom = expressionHelper.getFeatureOrNull(concreteFeature.getExpression()); } catch (TypeResolutionException e) { return false; // ignore: will be handled by type checking } @@ -267,8 +279,7 @@ private boolean isRequired(Feature feature) { return options.isRequired(); } - @Check - public void checkAmbiguousImplicitFeatureTypes(ViewTypeDefinition viewType) { + private Map> createSourceMap(ViewTypeDefinition viewType) { var sourceMap = new HashMap>(); BiConsumer register = (sourceType, query) -> { sourceMap.computeIfAbsent(sourceType, k -> new HashSet<>()).add(query); @@ -276,7 +287,7 @@ public void checkAmbiguousImplicitFeatureTypes(ViewTypeDefinition viewType) { // populate source mapping AstUtils.getAllQueries(viewType).forEach(query -> { - if (query instanceof MainQuery mainQuery) { + if (query instanceof ConcreteMainQuery mainQuery) { if (mainQuery.getSource() != null) { AstUtils.getAllFroms(mainQuery.getSource()).forEach(f -> { register.accept(f.getClazz(), mainQuery); @@ -290,35 +301,42 @@ public void checkAmbiguousImplicitFeatureTypes(ViewTypeDefinition viewType) { } }); + return sourceMap; + } + + @Check + public void checkAmbiguousImplicitFeatureTypes(ViewTypeDefinition viewType) { + var sourceMap = createSourceMap(viewType); + var alreadyChecked = new HashSet(); // to prevent infinite recursion because of cyclic references AstUtils.getAllQueries(viewType) .forEach(q -> checkQuery(q, sourceMap, alreadyChecked)); } private void checkQuery(Query query, Map> sourceMap, HashSet alreadyChecked) { - if (query.getBody() == null) { - if (query instanceof MainQuery mainQuery) { + var body = AstUtils.getBody(query); + if (body == null) { + if (query instanceof ConcreteMainQuery mainQuery) { // query without body and either no source or a source without joins, is not allowed, // but skip here because this is handled elsewhere if (mainQuery.getSource() != null && mainQuery.getSource().getJoins().isEmpty()) { checkCopiedClass(mainQuery.getSource().getFrom().getClazz(), sourceMap, query, alreadyChecked); } - } else { - var subQuery = (SubQuery) query; + } else if (query instanceof SubQuery subQuery) { var sourceType = AstUtils.inferSubQuerySourceType(subQuery, expressionHelper); if (sourceType != null) { checkCopiedClass(sourceType, sourceMap, query, alreadyChecked); } } - } else { - query.getBody().getFeatures().stream() + } else if (body instanceof ConcreteBody concreteBody) { + concreteBody.getFeatures().stream() .filter(f -> f.getType() == null) // explicit type -> not ambiguous .filter(f -> f.getSubQuery() == null) // subquery -> not ambiguous .forEach(f -> checkImplicitFeatureType(f, sourceMap, alreadyChecked)); } } - private void checkImplicitFeatureType(Feature feature, Map> sourceMap, HashSet alreadyChecked) { + private void checkImplicitFeatureType(ConcreteFeature feature, Map> sourceMap, HashSet alreadyChecked) { try { var inferredType = expressionHelper.inferEType(feature.getExpression()); if (inferredType == null || !(inferredType.classifier() instanceof EClass inferredClass)) { @@ -336,23 +354,23 @@ private void checkImplicitFeatureType(Feature feature, Map> s .collect(Collectors.joining(", ")) ), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } else if (candidates.size() == 1) { - if (candidates.iterator().next() instanceof MainQuery mainQuery && mainQuery.getSource() != null) { + if (candidates.iterator().next() instanceof ConcreteMainQuery mainQuery && mainQuery.getSource() != null) { if (!mainQuery.getSource().getJoins().isEmpty()) { warning( "Inferred type '%s' is a query with join which might be unintended and can lead to errors during transformation. Use explicit type to clarify the intended type.".formatted( AstUtils.getTargetName(mainQuery)), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } else if (!mainQuery.getSource().getGroupingExpressions().isEmpty()) { warning( "Inferred type '%s' is a query with group by which might be unintended and can lead to errors during transformation. Use explicit type to clarify the intended type.".formatted( AstUtils.getTargetName(mainQuery)), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } @@ -368,7 +386,6 @@ private void checkCopiedClass(EClass clazz, Map> sourceMap, Q return; } - for (var reference : clazz.getEAllReferences()) { var candidates = sourceMap.get(reference.getEReferenceType()); if (candidates == null) { // -> implicit copy of target class @@ -385,7 +402,7 @@ private void checkCopiedClass(EClass clazz, Map> sourceMap, Q null ); } else if (candidates.size() == 1) { - if (candidates.iterator().next() instanceof MainQuery mainQuery && mainQuery.getSource() != null) { + if (candidates.iterator().next() instanceof ConcreteMainQuery mainQuery && mainQuery.getSource() != null) { if (!mainQuery.getSource().getJoins().isEmpty()) { warning( ("Inferred target class '%s' for source class '%s' while copying reference '%s::%s' is " + @@ -417,7 +434,7 @@ private void checkCopiedClass(EClass clazz, Map> sourceMap, Q } @Check - public void checkRootFeatureCollision(MainQuery mainQuery) { + public void checkRootFeatureCollision(ConcreteMainQuery mainQuery) { if (!mainQuery.isRoot() || mainQuery.getBody() == null) { return; } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/NeoJoinValidator.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/NeoJoinValidator.java index 43365a09..35dffcfa 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/NeoJoinValidator.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/NeoJoinValidator.java @@ -6,7 +6,7 @@ import org.eclipse.xtext.validation.ComposedChecks; import tools.vitruv.neojoin.Constants; import tools.vitruv.neojoin.ast.AstPackage; -import tools.vitruv.neojoin.ast.MainQuery; +import tools.vitruv.neojoin.ast.ConcreteMainQuery; import tools.vitruv.neojoin.ast.ViewTypeDefinition; import tools.vitruv.neojoin.jvmmodel.ExpressionHelper; import tools.vitruv.neojoin.utils.AstUtils; @@ -40,7 +40,7 @@ public void duplicatedTargetClassName(ViewTypeDefinition viewType) { } }); - var hasImplicitRoot = viewType.getQueries().stream().noneMatch(MainQuery::isRoot); + var hasImplicitRoot = viewType.getQueries().stream().noneMatch(query -> query instanceof ConcreteMainQuery concreteMainQuery && concreteMainQuery.isRoot()); if (hasImplicitRoot) { var conflicts = groupedTargets.get(Constants.DefaultRootClassName); if (conflicts != null) { @@ -58,19 +58,19 @@ public void duplicatedTargetClassName(ViewTypeDefinition viewType) { } @Check - public void checkJoinWithoutBody(MainQuery mainQuery) { + public void checkJoinWithoutBody(ConcreteMainQuery mainQuery) { var hasJoin = mainQuery.getSource() != null && !mainQuery.getSource().getJoins().isEmpty(); if (hasJoin && mainQuery.getBody() == null) { - error("Query with a join must have a body", mainQuery, AstPackage.Literals.QUERY__BODY); + error("Query with a join must have a body", mainQuery, AstPackage.Literals.CONCRETE_MAIN_QUERY__BODY); } } @Check public void checkSingleRoot(ViewTypeDefinition viewType) { - var allRoots = viewType.getQueries().stream().filter(MainQuery::isRoot).toList(); + var allRoots = viewType.getQueries().stream().filter(query -> query instanceof ConcreteMainQuery concreteMainQuery && concreteMainQuery.isRoot()).toList(); if (allRoots.size() > 1) { for (var query : allRoots) { - error("Multiple root queries", query, AstPackage.Literals.MAIN_QUERY__ROOT); + error("Multiple root queries", query, AstPackage.Literals.CONCRETE_MAIN_QUERY__ROOT); } } } diff --git a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/SourceValidator.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/SourceValidator.java index 1de81b4a..2b1b85d1 100644 --- a/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/SourceValidator.java +++ b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/validation/SourceValidator.java @@ -2,9 +2,9 @@ import org.eclipse.xtext.validation.Check; import tools.vitruv.neojoin.ast.AstPackage; +import tools.vitruv.neojoin.ast.ConcreteMainQuery; import tools.vitruv.neojoin.ast.From; import tools.vitruv.neojoin.ast.JoinFeatureCondition; -import tools.vitruv.neojoin.ast.MainQuery; import tools.vitruv.neojoin.ast.Source; import tools.vitruv.neojoin.utils.AstUtils; @@ -13,7 +13,7 @@ public class SourceValidator extends ComposableValidator { @Check - public void checkUniqueAliases(MainQuery mainQuery) { + public void checkUniqueAliases(ConcreteMainQuery mainQuery) { if (mainQuery.getSource() == null) { return; } @@ -45,19 +45,19 @@ public void checkJoinConditionHasOtherInQueryWithMulti(JoinFeatureCondition cond } @Check - public void checkQueryWithoutSource(MainQuery mainQuery) { + public void checkQueryWithoutSource(ConcreteMainQuery mainQuery) { if (mainQuery.getSource() == null) { if (mainQuery.getName() == null) { error("Query without source must have a target name", mainQuery, AstPackage.Literals.QUERY__NAME); } if (mainQuery.getBody() == null) { - error("Query without source must have a body", mainQuery, AstPackage.Literals.QUERY__BODY); + error("Query without source must have a body", mainQuery, AstPackage.Literals.CONCRETE_MAIN_QUERY__BODY); } } } @Check - public void checkQueryWithAggregation(MainQuery mainQuery) { + public void checkQueryWithAggregation(ConcreteMainQuery mainQuery) { if (mainQuery.getSource() == null || mainQuery.getSource().getGroupingExpressions().isEmpty()) { return; } diff --git a/lang/frontend/language/src/main/xtext/tools/vitruv/neojoin/NeoJoin.xtext b/lang/frontend/language/src/main/xtext/tools/vitruv/neojoin/NeoJoin.xtext index d3e2f891..c184d007 100644 --- a/lang/frontend/language/src/main/xtext/tools/vitruv/neojoin/NeoJoin.xtext +++ b/lang/frontend/language/src/main/xtext/tools/vitruv/neojoin/NeoJoin.xtext @@ -16,10 +16,18 @@ Export: Import: 'import' package=[ecore::EPackage|STRING] ('as' alias=ID)?; -MainQuery: +MainQuery: ConcreteMainQuery | AbstractMainQuery; + +ConcreteMainQuery: source=Source? 'create' root?='root'? name=ID? - body=Body?; + ('extends' superClasses+=[ecore::EObject|QualifiedName] (',' superClasses+=[ecore::EObject|QualifiedName])*)? + body=ConcreteBody?; + +AbstractMainQuery: + 'abstract' name=ID + ('extends' superClasses+=[ecore::EObject|QualifiedName] (',' superClasses+=[ecore::EObject|QualifiedName])*)? + body=AbstractBody; Source: 'from' from=From @@ -42,10 +50,13 @@ JoinFeatureCondition: JoinExpressionCondition: 'on' expression=XOrExpression; -Body: - {Body} '{' features+=Feature* '}'; // {Body} ensures that the object is created to differentiate an empty body {} from a missing body +ConcreteBody: + {ConcreteBody} '{' features+=ConcreteFeature* '}'; // {ConcreteBody} ensures that the object is created to differentiate an empty body {} from a missing body -Feature: +AbstractBody: + {AbstractBody} '{' features+=AbstractFeature* '}'; + +ConcreteFeature: ( name=ID (':' type=[ecore::EObject|QualifiedName])? // type refers either to an EDataType or a Query @@ -55,6 +66,11 @@ Feature: expression=XOrExpression subQuery=SubQuery?; +AbstractFeature: + name=ID + ':' type=[ecore::EObject|QualifiedName] // type refers either to an EDataType or a Query + (hasModifiers?='[' (modifiers+=Modifier (',' modifiers+=Modifier)*)? ']')?; // allow empty modifier list in grammar and forbid it using a validator for improved error messages + enum FeatureOp: COPY='=' | CALCULATE=':='; @@ -74,6 +90,9 @@ MultiplicityExpr: {MultiplicityManyAtLeast} lowerBound=INT '..*'; SubQuery: - 'create' (name=ID ->body=Body? | body=Body); + 'create' (name=ID ->body=ConcreteBody? | body=ConcreteBody); -Query: MainQuery | SubQuery; // for type hierarchy +// for type hierarchy: +Query: MainQuery | SubQuery; +Body: ConcreteBody | AbstractBody; +Feature: ConcreteFeature | AbstractFeature; diff --git a/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRFeature.java b/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRFeature.java index 0f89a1cd..fe313cc1 100644 --- a/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRFeature.java +++ b/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRFeature.java @@ -47,7 +47,7 @@ record Calculate(XExpression expression) implements Kind {} */ sealed interface Copy extends Kind { - @Override + @java.lang.Override EStructuralFeature source(); /** @@ -76,6 +76,22 @@ record Implicit(EStructuralFeature source) implements Copy {} */ record Generate() implements Kind {} + /** + * The feature overrides a feature in a super classs. + * + * @param overridden overridden feature in a super class + * @param overriding + */ + record Override(AQRFeature overridden, AQRFeature.Kind overriding) implements Kind { + + @java.lang.Override + public XExpression expression() { + return overriding.expression(); + } + + } + + record Abstract() implements Kind {} } /** @@ -151,6 +167,14 @@ public String toString() { return "Attribute[name='%s', type=%s, kind=%s, options=%s]".formatted(name, type.getName(), kind, options); } + Attribute withFeatureKind(Kind kind) { + return new Attribute(name, type, kind, options); + } + + Attribute withOptions(Options options) { + return new Attribute(name, type, kind, options); + } + } /** @@ -173,6 +197,14 @@ public String toString() { return "Reference[name='%s', type=%s, kind=%s, options=%s]".formatted(name, type.name(), kind, options); } + Reference withFeatureKind(Kind kind) { + return new Reference(name, type, kind, options); + } + + Reference withOptions(Options options) { + return new Reference(name, type, kind, options); + } + } } diff --git a/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRTargetClass.java b/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRTargetClass.java index 12a78afe..42a454de 100644 --- a/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRTargetClass.java +++ b/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRTargetClass.java @@ -2,7 +2,11 @@ import org.jspecify.annotations.Nullable; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; +import java.util.Set; /** * Class within the target model. @@ -19,7 +23,9 @@ public final class AQRTargetClass { private final String name; + private final boolean isAbstract; private final @Nullable AQRSource source; + private final List superClasses; private final List features; /** @@ -29,11 +35,15 @@ public final class AQRTargetClass { */ public AQRTargetClass( String name, + boolean isAbstract, @Nullable AQRSource source, + List superClasses, List features ) { this.name = name; + this.isAbstract = isAbstract; this.source = source; + this.superClasses = superClasses; this.features = features; } @@ -41,19 +51,45 @@ public String name() { return name; } + public boolean isAbstract() { + return isAbstract; + } + public @Nullable AQRSource source() { return source; } + public List superClasses() { + return superClasses; + } + + public List allSuperClasses() { + // BFS + Queue queue = new LinkedList<>(superClasses()); + Set allSuperClasses = new HashSet<>(); + + while (!queue.isEmpty()) { + var superClass = queue.poll(); + allSuperClasses.add(superClass); + + var newSuperClasses = superClass.superClasses(); + newSuperClasses.removeAll(allSuperClasses); + queue.addAll(newSuperClasses); + } + + return allSuperClasses.stream().toList(); + } + public List features() { return features; } @Override public String toString() { - return "TargetClass[name=%s, source=%s, features=%s]".formatted( + return "TargetClass[name=%s, source=%s, superClasses=%s, features=%s]".formatted( name, source, + superClasses, features ); } diff --git a/vscode-plugin/package.json b/vscode-plugin/package.json index 3f33050e..1561fe48 100644 --- a/vscode-plugin/package.json +++ b/vscode-plugin/package.json @@ -34,7 +34,7 @@ { "language": "neojoin", "scopeName": "source.neojoin", - "path": "./src/language/neojoin.tmGrammar.json" + "path": "./src/language/neojoin.tmLanguage.json" } ], "commands": [ diff --git a/vscode-plugin/src/language/neojoin.tmGrammar.json b/vscode-plugin/src/language/neojoin.tmLanguage.json similarity index 69% rename from vscode-plugin/src/language/neojoin.tmGrammar.json rename to vscode-plugin/src/language/neojoin.tmLanguage.json index 6c3466d7..7ed5d361 100644 --- a/vscode-plugin/src/language/neojoin.tmGrammar.json +++ b/vscode-plugin/src/language/neojoin.tmLanguage.json @@ -1,13 +1,9 @@ { "patterns": [ { - "match": "\\b(as|by|case|catch|containment|create|default|derived|do|else|export|extends|extension|false|finally|for|from|group|id|if|import|inner|instanceof|join|left|new|null|on|ordered|readonly|return|root|static|super|switch|synchronized|throw|to|transient|true|try|typeof|unique|unsettable|using|val|var|volatile|where|while|with)\\b", + "match": "\\b(abstract|as|by|case|catch|changeable|containment|create|default|derived|do|else|export|extends|extension|false|finally|for|from|group|id|if|import|inner|instanceof|join|left|new|null|on|ordered|return|root|static|super|switch|synchronized|throw|to|transient|true|try|typeof|unique|unsettable|using|val|var|volatile|where|while|with)\\b", "name": "keyword.control.neojoin" }, - { - "match": "\\\"(\\\\.|[^\\\\\\\"])*\\\"", - "name": "string.quoted.neojoin" - }, { "match": "[0-9]([0-9]|_)*", "name": "constant.numeric.neojoin" @@ -16,6 +12,10 @@ "match": "\\^?([a-z]|[A-Z]|\\$|_)([a-z]|[A-Z]|\\$|_|[0-9])*", "name": "variable.neojoin" }, + { + "match": "(\\\"(\\\\.|[^\\\\\\\"])*\\\"?|\u0027(\\\\.|[^\\\\\u0027])*\u0027?)", + "name": "string.quoted.neojoin" + }, { "begin": "/\\*", "end": "\\*/",