From bdcf6138d7017ec57220a4ce2fae4134ec9c4b7d Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:31:04 +0100 Subject: [PATCH 01/14] add syntax for abstract classes, extensions, and abstract features --- .../neojoin/scoping/NeoJoinScopeProvider.java | 2 + .../neojoin/validation/SourceValidator.java | 2 +- .../xtext/tools/vitruv/neojoin/NeoJoin.xtext | 19 +++++++++- vscode-plugin/package.json | 2 +- .../src/language/neojoin.tmGrammar.json | 38 ------------------- .../src/language/neojoin.tmLanguage.json | 34 +++++++++++++++++ 6 files changed, 55 insertions(+), 42 deletions(-) delete mode 100644 vscode-plugin/src/language/neojoin.tmGrammar.json create mode 100644 vscode-plugin/src/language/neojoin.tmLanguage.json 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 df6be45b..5210056f 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 @@ -80,6 +80,8 @@ public IScope getScope(EObject context, EReference reference) { } } else if (reference == AstPackage.Literals.FEATURE__TYPE) { return createFeatureTypeScope(AstUtils.getViewType(context)); + } else if (reference == AstPackage.Literals.ABSTRACT_FEATURE__TYPE) { + return createFeatureTypeScope(AstUtils.getViewType(context)); } else { return super.getScope(context, reference); } 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..2488fe58 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 @@ -50,7 +50,7 @@ public void checkQueryWithoutSource(MainQuery mainQuery) { if (mainQuery.getName() == null) { error("Query without source must have a target name", mainQuery, AstPackage.Literals.QUERY__NAME); } - if (mainQuery.getBody() == null) { + if (mainQuery.getBody() == null && mainQuery.getAbstractBody() == null) { error("Query without source must have a body", mainQuery, AstPackage.Literals.QUERY__BODY); } } 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..4a42da2d 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 @@ -18,8 +18,15 @@ Import: MainQuery: source=Source? - 'create' root?='root'? name=ID? - body=Body?; + ( + 'create' root?='root'? name=ID? + ('extends' superClasses+=QualifiedName (',' superClasses+=QualifiedName)*)? + body=Body? + )|( + 'abstract' name=ID? + ('extends' superClasses+=QualifiedName (',' superClasses+=QualifiedName)*)? + abstractBody=AbstractBody + ); Source: 'from' from=From @@ -45,6 +52,9 @@ JoinExpressionCondition: Body: {Body} '{' features+=Feature* '}'; // {Body} ensures that the object is created to differentiate an empty body {} from a missing body +AbstractBody: + {AbstractBody} '{' abstractFeatures+=AbstractFeature* '}'; + Feature: ( name=ID @@ -55,6 +65,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=':='; diff --git a/vscode-plugin/package.json b/vscode-plugin/package.json index 18dfdcba..bc429f8c 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.tmGrammar.json deleted file mode 100644 index 6c3466d7..00000000 --- a/vscode-plugin/src/language/neojoin.tmGrammar.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "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", - "name": "keyword.control.neojoin" - }, - { - "match": "\\\"(\\\\.|[^\\\\\\\"])*\\\"", - "name": "string.quoted.neojoin" - }, - { - "match": "[0-9]([0-9]|_)*", - "name": "constant.numeric.neojoin" - }, - { - "match": "\\^?([a-z]|[A-Z]|\\$|_)([a-z]|[A-Z]|\\$|_|[0-9])*", - "name": "variable.neojoin" - }, - { - "begin": "/\\*", - "end": "\\*/", - "name": "comment.block.neojoin" - }, - { - "match": "//[^\\\n\\\r]*(\\\r?\\\n)?", - "name": "comment.line.neojoin" - }, - { - "match": "(!|!\u003d|!\u003d\u003d|#|%|%\u003d|\u0026|\u0026\u0026|\\(|\\)|\\*|\\*\\*|\\*\u003d|\\+|\\+\\+|\\+\u003d|,|-|--|-\u003d|-\u003e|\\.|\\.\\.|\\.\\.\\*|\\.\\.\u003c|/|/\u003d|:|::|:\u003d|;|\u003c|\u003c\u003e|\u003d|\u003d\u003d|\u003d\u003d\u003d|\u003d\u003e|\u003e|\u003e\u003d|\\?|\\?\\.|\\?:|\\[|\\]|\\{|\\||\\|\\||\\})", - "name": "punctuation.neojoin" - }, - { - "match": ".", - "name": "invalid.neojoin" - } - ], - "scopeName": "source.neojoin" -} diff --git a/vscode-plugin/src/language/neojoin.tmLanguage.json b/vscode-plugin/src/language/neojoin.tmLanguage.json new file mode 100644 index 00000000..070d7b76 --- /dev/null +++ b/vscode-plugin/src/language/neojoin.tmLanguage.json @@ -0,0 +1,34 @@ +{ + "patterns": [ + { + "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": "[0-9]([0-9]|_)*", + "name": "constant.numeric.neojoin" + }, + { + "match": "\\^?([a-z]|[A-Z]|\\$|_)([a-z]|[A-Z]|\\$|_|[0-9])*", + "name": "variable.neojoin" + }, + { + "match": "(\\\"(\\\\.|[^\\\\\\\"])*\\\"?|\u0027(\\\\.|[^\\\\\u0027])*\u0027?)", + "name": "string.quoted.neojoin" + }, + { + "begin": "/\\*", + "end": "\\*/", + "name": "comment.block.neojoin" + }, + { + "match": "//[^\\\n\\\r]*(\\\r?\\\n)?", + "name": "comment.line.neojoin" + }, + { + "match": ".", + "name": "invalid.neojoin" + } + ], + "scopeName": "source.neojoin" +} \ No newline at end of file From cb1db973ad43b2e9ce9228464f19d2aaa185cbef Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:02:14 +0100 Subject: [PATCH 02/14] add support for super classes and overwriting features to AQR and meta-model generation --- .../visualization/VisualizationGenerator.java | 12 ++++- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 52 +++++++++++++++---- .../generation/MetaModelGenerator.java | 6 ++- .../tools/vitruv/neojoin/aqr/AQRFeature.java | 1 + .../vitruv/neojoin/aqr/AQRTargetClass.java | 31 ++++++++++- 5 files changed, 88 insertions(+), 14 deletions(-) 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 dbc2f8e6..6efcf1e5 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 @@ -141,14 +141,16 @@ public String generate() { // show selected target classes 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,6 +197,12 @@ 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); 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 796d1873..d46fab90 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 @@ -64,7 +64,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 +90,16 @@ 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()); + } + + while (!populationQueue.isEmpty()) { var entry = populationQueue.poll(); //noinspection DataFlowIssue - false positive - populateTargetClass(entry.left(), entry.right()); + populateTargetClass(entry.left(), entry.right() == null ? null : entry.right().getBody()); } var root = createRootIfNeededAndInit(); @@ -128,7 +134,7 @@ private AQRTargetClass getTargetForQuery(Query query) { } private AQRTargetClass createTargetClass(String name, @Nullable AQRSource source, @Nullable Query query) { - var target = new AQRTargetClass(name, source, new ArrayList<>()); + var target = new AQRTargetClass(name, source, new ArrayList<>(), new ArrayList<>()); targetClasses.add(target); @@ -143,7 +149,7 @@ 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; } @@ -191,6 +197,23 @@ private AQRTargetClass getOrCreateTargetClass(EClass source) { return targets.iterator().next(); } + private void addSuperClassesToTargetClass(AQRTargetClass targetClazz, @Nullable Query query) { + if (query != null) { + switch (query) { + case MainQuery mainQuery -> { + for (String superClassName : mainQuery.getSuperClasses()) { + var superClassCandidates = targetClasses.stream().filter(targetClass -> targetClass.name().equals(superClassName)).toList(); + invariant(!superClassCandidates.isEmpty(), "Classes can only extend target classes"); + invariant(superClassCandidates.size() == 1, "Class names must be unique"); + + targetClazz.superClasses().add(superClassCandidates.getFirst()); + } + } + default -> {} + } + } + } + /** * Populates the target class with features specified in the given body or copies all features if no body was given. * @@ -200,7 +223,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()) { - targetClazz.features().add(createFeature(feature)); + targetClazz.features().add(createFeature(targetClazz, feature)); } } else { // copy all features from the source class invariant(targetClazz.source() != null, "Query without source must have a body"); @@ -236,8 +259,8 @@ private AQRFeature copyFeature(EStructuralFeature feature) { /** * Create a feature based on the given definition. */ - private AQRFeature createFeature(Feature feature) { - var kind = getFeatureKind(feature); + private AQRFeature createFeature(AQRTargetClass targetClass, Feature feature) { + var kind = getFeatureKind(targetClass, feature); var name = getFeatureName(feature, kind); var inferredType = inferType(feature.getExpression()); @@ -257,8 +280,8 @@ private AQRFeature createFeature(Feature feature) { /** * Determines the {@link AQRFeature.Kind kind} of the feature. */ - private AQRFeature.Kind getFeatureKind(Feature feature) { - return switch (feature.getOp()) { + private AQRFeature.Kind getFeatureKind(AQRTargetClass targetClass, Feature feature) { + AQRFeature.Kind defKind = switch (feature.getOp()) { case COPY -> { try { var type = expressionHelper.getFeatureOrNull(feature.getExpression()); @@ -273,6 +296,14 @@ private AQRFeature.Kind getFeatureKind(Feature feature) { } case CALCULATE -> new AQRFeature.Kind.Calculate(feature.getExpression()); }; + + var overwrittenFeature = targetClass.allSuperClasses().stream().flatMap(superClass -> superClass.features().stream()).filter(inheritedFeature -> inheritedFeature.name().equals(getFeatureName(feature, defKind))).findAny(); + if (overwrittenFeature.isPresent()) { + invariant(feature.getModifiers().isEmpty(), "Overwriting features may not have modifiers"); + return new AQRFeature.Kind.Overwrite(overwrittenFeature.get(), feature.getExpression()); + } else { + return defKind; + } } /** @@ -287,6 +318,7 @@ private static String getFeatureName(Feature feature, AQRFeature.Kind kind) { case AQRFeature.Kind.Copy copy -> copy.source().getName(); case AQRFeature.Kind.Calculate ignored -> invariantFailed("Calculated feature must have a name: " + feature.getExpression()); + case AQRFeature.Kind.Overwrite overwriding -> overwriding.overwritten().name(); default -> fail(); }; } @@ -389,7 +421,7 @@ private AQRTargetClass createRootIfNeededAndInit() { if (rootQuery != null) { root = getTargetForQuery(rootQuery); } else { - root = new AQRTargetClass(Constants.DefaultRootClassName, null, new ArrayList<>()); + root = new AQRTargetClass(Constants.DefaultRootClassName, null, new ArrayList<>(), new ArrayList<>()); targetClasses.add(root); } 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 9c95089e..b694731c 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 @@ -105,7 +105,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.Overwrite)).map(this::createFeature).toList(); target.getEStructuralFeatures().addAll(features); } 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..31699568 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 @@ -76,6 +76,7 @@ record Implicit(EStructuralFeature source) implements Copy {} */ record Generate() implements Kind {} + record Overwrite(AQRFeature overwritten, XExpression expression) implements Kind {} } /** 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..e94aba1a 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. @@ -20,6 +24,7 @@ public final class AQRTargetClass { private final String name; private final @Nullable AQRSource source; + private final List superClasses; private final List features; /** @@ -30,10 +35,12 @@ public final class AQRTargetClass { public AQRTargetClass( String name, @Nullable AQRSource source, + List superClasses, List features ) { this.name = name; this.source = source; + this.superClasses = superClasses; this.features = features; } @@ -45,15 +52,37 @@ public String name() { 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 ); } From c9cddedf2a93daa775fab4dd6ee7467cac0f963a Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:02:31 +0100 Subject: [PATCH 03/14] add scope provider for super classes --- .../vitruv/neojoin/scoping/NeoJoinScopeProvider.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 5210056f..b91cfec3 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 @@ -78,6 +78,8 @@ public IScope getScope(EObject context, EReference reference) { if (left != null && right != null) { return createJoinConditionFieldsScope(left, right); } + } else if (reference == AstPackage.Literals.MAIN_QUERY__SUPER_CLASSES) { + createSuperClassScope(AstUtils.getViewType(context)); } else if (reference == AstPackage.Literals.FEATURE__TYPE) { return createFeatureTypeScope(AstUtils.getViewType(context)); } else if (reference == AstPackage.Literals.ABSTRACT_FEATURE__TYPE) { @@ -139,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. */ From ed9009c5fcb09d9b74890fa8695dd8d90bbf4a89 Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:06:56 +0100 Subject: [PATCH 04/14] fix detection of overwritten features and validate inheritance --- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 68 +++++++++++++++---- .../tools/vitruv/neojoin/aqr/AQRFeature.java | 14 ++++ 2 files changed, 68 insertions(+), 14 deletions(-) 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 d46fab90..35af7495 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 @@ -95,13 +95,18 @@ public AQR build() { addSuperClassesToTargetClass(entry.left(), entry.right()); } - + // populate target classes while (!populationQueue.isEmpty()) { var entry = populationQueue.poll(); //noinspection DataFlowIssue - false positive populateTargetClass(entry.left(), entry.right() == null ? null : entry.right().getBody()); } + // identify and verify inheritance + for (var targetClass : targetClasses) { + applyInheritance(targetClass); + } + var root = createRootIfNeededAndInit(); return new AQR( @@ -223,7 +228,7 @@ private void addSuperClassesToTargetClass(AQRTargetClass targetClazz, @Nullable private void populateTargetClass(AQRTargetClass targetClazz, @Nullable Body body) { if (body != null) { // create features based on definition in Body for (Feature feature : body.getFeatures()) { - targetClazz.features().add(createFeature(targetClazz, feature)); + targetClazz.features().add(createFeature(feature)); } } else { // copy all features from the source class invariant(targetClazz.source() != null, "Query without source must have a body"); @@ -259,8 +264,8 @@ private AQRFeature copyFeature(EStructuralFeature feature) { /** * Create a feature based on the given definition. */ - private AQRFeature createFeature(AQRTargetClass targetClass, Feature feature) { - var kind = getFeatureKind(targetClass, feature); + private AQRFeature createFeature(Feature feature) { + var kind = getFeatureKind(feature); var name = getFeatureName(feature, kind); var inferredType = inferType(feature.getExpression()); @@ -280,8 +285,8 @@ private AQRFeature createFeature(AQRTargetClass targetClass, Feature feature) { /** * Determines the {@link AQRFeature.Kind kind} of the feature. */ - private AQRFeature.Kind getFeatureKind(AQRTargetClass targetClass, Feature feature) { - AQRFeature.Kind defKind = switch (feature.getOp()) { + private AQRFeature.Kind getFeatureKind(Feature feature) { + return switch (feature.getOp()) { case COPY -> { try { var type = expressionHelper.getFeatureOrNull(feature.getExpression()); @@ -296,14 +301,6 @@ private AQRFeature.Kind getFeatureKind(AQRTargetClass targetClass, Feature featu } case CALCULATE -> new AQRFeature.Kind.Calculate(feature.getExpression()); }; - - var overwrittenFeature = targetClass.allSuperClasses().stream().flatMap(superClass -> superClass.features().stream()).filter(inheritedFeature -> inheritedFeature.name().equals(getFeatureName(feature, defKind))).findAny(); - if (overwrittenFeature.isPresent()) { - invariant(feature.getModifiers().isEmpty(), "Overwriting features may not have modifiers"); - return new AQRFeature.Kind.Overwrite(overwrittenFeature.get(), feature.getExpression()); - } else { - return defKind; - } } /** @@ -414,6 +411,49 @@ private TypeInfo inferType(XExpression expression) { } } + private void applyInheritance(AQRTargetClass targetClass) { + var superClassFeatures = targetClass.allSuperClasses().stream().flatMap(superClass -> superClass.features().stream()).toList(); + + // update overwriting features + targetClass.features().replaceAll(feature -> { + var overwrittenFeatures = superClassFeatures.stream().filter(superClassFeature -> superClassFeature.name().equals(feature.name())).toList(); + if (overwrittenFeatures.isEmpty()) { + return feature; + } + + invariant(overwrittenFeatures.size() == 1); + var overwrittenFeature = overwrittenFeatures.getFirst(); + + invariant(feature.options().equals(overwrittenFeature.options()), "Overwriting features may not change modifiers"); + + switch (feature) { + case AQRFeature.Attribute attribute -> { + invariant(overwrittenFeature instanceof AQRFeature.Attribute, "Attribute must overwrite attribute"); + var overwrittenAttribute = (AQRFeature.Attribute)overwrittenFeature; + invariant(overwrittenAttribute.type().equals(attribute.type()), "Type of overwriting feature must be equal to type of overwritten feature"); + + return attribute.setFeatureKind(new AQRFeature.Kind.Overwrite(overwrittenFeature, feature.kind().expression())); + } + case AQRFeature.Reference reference -> { + invariant(overwrittenFeature instanceof AQRFeature.Reference, "Reference must overwrite reference"); + var overwrittenReference = (AQRFeature.Reference)overwrittenFeature; + invariant(overwrittenReference.type().equals(reference.type()), "Type of overwriting feature must be equal to type of overwritten feature"); + + return reference.setFeatureKind(new AQRFeature.Kind.Overwrite(overwrittenFeature, feature.kind().expression())); + } + } + }); + + // check if all inherited features are overwritten + var missingFeatures = superClassFeatures.stream().filter(superClassFeature -> !targetClass.features().stream().filter(feature -> + feature.name().equals(superClassFeature.name()) && + (feature.kind() instanceof AQRFeature.Kind.Overwrite) + ).findAny().isPresent()).toList(); + if (!missingFeatures.isEmpty()) { + invariant(!missingFeatures.isEmpty(), "Sub classes must overwrite all inherited features"); + } + } + private AQRTargetClass createRootIfNeededAndInit() { AQRTargetClass root; 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 31699568..c2373d12 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 @@ -76,6 +76,12 @@ record Implicit(EStructuralFeature source) implements Copy {} */ record Generate() implements Kind {} + /** + * The feature overwrites a feature in a super classs. + * + * @param overwritten overwritten feature in a super class + * @param expression expression calculating the value of the feature (not inherited) + */ record Overwrite(AQRFeature overwritten, XExpression expression) implements Kind {} } @@ -152,6 +158,10 @@ public String toString() { return "Attribute[name='%s', type=%s, kind=%s, options=%s]".formatted(name, type.getName(), kind, options); } + Attribute setFeatureKind(Kind kind) { + return new Attribute(name, type, kind, options); + } + } /** @@ -174,6 +184,10 @@ public String toString() { return "Reference[name='%s', type=%s, kind=%s, options=%s]".formatted(name, type.name(), kind, options); } + Reference setFeatureKind(Kind kind) { + return new Reference(name, type, kind, options); + } + } } From 48e7c902a8d7155ac4fdc4e77e35088b36df56cc Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:04:10 +0100 Subject: [PATCH 05/14] small fixes --- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 52 ++++++++----------- .../generation/MetaModelGenerator.java | 2 +- .../tools/vitruv/neojoin/aqr/AQRFeature.java | 8 +-- .../src/language/neojoin.tmLanguage.json | 6 ++- 4 files changed, 33 insertions(+), 35 deletions(-) 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 f446d26a..08265fd1 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 @@ -203,18 +203,13 @@ private AQRTargetClass getOrCreateTargetClass(EClass source) { } private void addSuperClassesToTargetClass(AQRTargetClass targetClazz, @Nullable Query query) { - if (query != null) { - switch (query) { - case MainQuery mainQuery -> { - for (String superClassName : mainQuery.getSuperClasses()) { - var superClassCandidates = targetClasses.stream().filter(targetClass -> targetClass.name().equals(superClassName)).toList(); - invariant(!superClassCandidates.isEmpty(), "Classes can only extend target classes"); - invariant(superClassCandidates.size() == 1, "Class names must be unique"); - - targetClazz.superClasses().add(superClassCandidates.getFirst()); - } - } - default -> {} + if (query != null && query instanceof MainQuery mainQuery) { + for (String superClassName : mainQuery.getSuperClasses()) { + var superClassCandidates = targetClasses.stream().filter(targetClass -> targetClass.name().equals(superClassName)).toList(); + invariant(!superClassCandidates.isEmpty(), "Classes can only extend target classes"); + invariant(superClassCandidates.size() == 1, "Class names must be unique"); + + targetClazz.superClasses().add(superClassCandidates.get(0)); } } } @@ -313,8 +308,8 @@ private static String getFeatureName(Feature feature, AQRFeature.Kind kind) { return copy.source().getName(); } else if (kind instanceof AQRFeature.Kind.Calculate) { return invariantFailed("Calculated feature must have a name: " + feature.getExpression()); - } else if (kind instanceof AQRFeature.Kind.Overwrite overwriding ) { - return overwriding.overwritten().name(); + } else if (kind instanceof AQRFeature.Kind.Override overriding) { + return overriding.overwritten().name(); } else { return fail(); } @@ -422,32 +417,31 @@ private void applyInheritance(AQRTargetClass targetClass) { } invariant(overwrittenFeatures.size() == 1); - var overwrittenFeature = overwrittenFeatures.getFirst(); + var overwrittenFeature = overwrittenFeatures.get(0); invariant(feature.options().equals(overwrittenFeature.options()), "Overwriting features may not change modifiers"); - switch (feature) { - case AQRFeature.Attribute attribute -> { - invariant(overwrittenFeature instanceof AQRFeature.Attribute, "Attribute must overwrite attribute"); - var overwrittenAttribute = (AQRFeature.Attribute)overwrittenFeature; - invariant(overwrittenAttribute.type().equals(attribute.type()), "Type of overwriting feature must be equal to type of overwritten feature"); + if (feature instanceof AQRFeature.Attribute attribute) { + invariant(overwrittenFeature instanceof AQRFeature.Attribute, "Attribute must overwrite attribute"); + var overwrittenAttribute = (AQRFeature.Attribute)overwrittenFeature; + invariant(overwrittenAttribute.type().equals(attribute.type()), "Type of overwriting feature must be equal to type of overwritten feature"); - return attribute.setFeatureKind(new AQRFeature.Kind.Overwrite(overwrittenFeature, feature.kind().expression())); - } - case AQRFeature.Reference reference -> { - invariant(overwrittenFeature instanceof AQRFeature.Reference, "Reference must overwrite reference"); - var overwrittenReference = (AQRFeature.Reference)overwrittenFeature; - invariant(overwrittenReference.type().equals(reference.type()), "Type of overwriting feature must be equal to type of overwritten feature"); + return attribute.withFeatureKind(new AQRFeature.Kind.Override(overwrittenFeature, feature.kind().expression())); + } else if (feature instanceof AQRFeature.Reference reference) { + invariant(overwrittenFeature instanceof AQRFeature.Reference, "Reference must overwrite reference"); + var overwrittenReference = (AQRFeature.Reference)overwrittenFeature; + invariant(overwrittenReference.type().equals(reference.type()), "Type of overwriting feature must be equal to type of overwritten feature"); - return reference.setFeatureKind(new AQRFeature.Kind.Overwrite(overwrittenFeature, feature.kind().expression())); - } + return reference.withFeatureKind(new AQRFeature.Kind.Override(overwrittenFeature, feature.kind().expression())); + } else { + throw new IllegalStateException("AQRFeature is a sealed interface therefore this check should be exhaustive"); } }); // check if all inherited features are overwritten var missingFeatures = superClassFeatures.stream().filter(superClassFeature -> !targetClass.features().stream().filter(feature -> feature.name().equals(superClassFeature.name()) && - (feature.kind() instanceof AQRFeature.Kind.Overwrite) + (feature.kind() instanceof AQRFeature.Kind.Override) ).findAny().isPresent()).toList(); if (!missingFeatures.isEmpty()) { invariant(!missingFeatures.isEmpty(), "Sub classes must overwrite all inherited features"); 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 75cea78f..8005bc2d 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 @@ -109,7 +109,7 @@ private void populateClass(AQRTargetClass targetClass) { 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.Overwrite)).map(this::createFeature).toList(); + 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/model/src/main/java/tools/vitruv/neojoin/aqr/AQRFeature.java b/lang/model/src/main/java/tools/vitruv/neojoin/aqr/AQRFeature.java index c2373d12..dd2b82db 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(); /** @@ -82,7 +82,7 @@ record Generate() implements Kind {} * @param overwritten overwritten feature in a super class * @param expression expression calculating the value of the feature (not inherited) */ - record Overwrite(AQRFeature overwritten, XExpression expression) implements Kind {} + record Override(AQRFeature overwritten, XExpression expression) implements Kind {} } /** @@ -158,7 +158,7 @@ public String toString() { return "Attribute[name='%s', type=%s, kind=%s, options=%s]".formatted(name, type.getName(), kind, options); } - Attribute setFeatureKind(Kind kind) { + Attribute withFeatureKind(Kind kind) { return new Attribute(name, type, kind, options); } @@ -184,7 +184,7 @@ public String toString() { return "Reference[name='%s', type=%s, kind=%s, options=%s]".formatted(name, type.name(), kind, options); } - Reference setFeatureKind(Kind kind) { + Reference withFeatureKind(Kind kind) { return new Reference(name, type, kind, options); } diff --git a/vscode-plugin/src/language/neojoin.tmLanguage.json b/vscode-plugin/src/language/neojoin.tmLanguage.json index 070d7b76..7ed5d361 100644 --- a/vscode-plugin/src/language/neojoin.tmLanguage.json +++ b/vscode-plugin/src/language/neojoin.tmLanguage.json @@ -25,10 +25,14 @@ "match": "//[^\\\n\\\r]*(\\\r?\\\n)?", "name": "comment.line.neojoin" }, + { + "match": "(!|!\u003d|!\u003d\u003d|#|%|%\u003d|\u0026|\u0026\u0026|\\(|\\)|\\*|\\*\\*|\\*\u003d|\\+|\\+\\+|\\+\u003d|,|-|--|-\u003d|-\u003e|\\.|\\.\\.|\\.\\.\\*|\\.\\.\u003c|/|/\u003d|:|::|:\u003d|;|\u003c|\u003c\u003e|\u003d|\u003d\u003d|\u003d\u003d\u003d|\u003d\u003e|\u003e|\u003e\u003d|\\?|\\?\\.|\\?:|\\[|\\]|\\{|\\||\\|\\||\\})", + "name": "punctuation.neojoin" + }, { "match": ".", "name": "invalid.neojoin" } ], "scopeName": "source.neojoin" -} \ No newline at end of file +} From d4aa15a7d11aebce60d7f01f34fa497e121db714 Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:32:08 +0100 Subject: [PATCH 06/14] add meta-level support for abstract target classes --- .../ide/visualization/PlantUMLBuilder.java | 4 +- .../visualization/VisualizationGenerator.java | 2 +- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 111 +++++++++++------- .../generation/MetaModelGenerator.java | 1 + .../neojoin/jvmmodel/QueryModelInferrer.java | 17 +-- .../neojoin/scoping/NeoJoinScopeProvider.java | 10 +- .../tools/vitruv/neojoin/utils/AstUtils.java | 64 ++++++++-- .../validation/FeatureModifierValidator.java | 43 +++---- .../neojoin/validation/FeatureValidator.java | 102 +++++++++------- .../neojoin/validation/NeoJoinValidator.java | 12 +- .../neojoin/validation/SourceValidator.java | 12 +- .../xtext/tools/vitruv/neojoin/NeoJoin.xtext | 36 +++--- .../tools/vitruv/neojoin/aqr/AQRFeature.java | 2 + .../vitruv/neojoin/aqr/AQRTargetClass.java | 7 ++ 14 files changed, 264 insertions(+), 159 deletions(-) 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 4005bcd8..86ecbaad 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 @@ -236,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); 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 08265fd1..3f0e6772 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; @@ -99,7 +103,7 @@ public AQR build() { while (!populationQueue.isEmpty()) { var entry = populationQueue.poll(); //noinspection DataFlowIssue - false positive - populateTargetClass(entry.left(), entry.right() == null ? null : entry.right().getBody()); + populateTargetClass(entry.left(), entry.right() == null ? null : AstUtils.getBody(entry.right())); } // identify and verify inheritance @@ -138,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<>(), 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); @@ -159,17 +163,26 @@ private AQRTargetClass createTargetClass(String name, @Nullable AQRSource source } 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 ); @@ -188,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( @@ -204,12 +217,10 @@ private AQRTargetClass getOrCreateTargetClass(EClass source) { private void addSuperClassesToTargetClass(AQRTargetClass targetClazz, @Nullable Query query) { if (query != null && query instanceof MainQuery mainQuery) { - for (String superClassName : mainQuery.getSuperClasses()) { - var superClassCandidates = targetClasses.stream().filter(targetClass -> targetClass.name().equals(superClassName)).toList(); - invariant(!superClassCandidates.isEmpty(), "Classes can only extend target classes"); - invariant(superClassCandidates.size() == 1, "Class names must be unique"); + for (EObject superClass : mainQuery.getSuperClasses()) { + invariant(superClass instanceof Query, "Classes can only extend classes created from other queries"); - targetClazz.superClasses().add(superClassCandidates.get(0)); + targetClazz.superClasses().add(getTargetForQuery((Query) superClass)); } } } @@ -222,7 +233,7 @@ private void addSuperClassesToTargetClass(AQRTargetClass targetClazz, @Nullable */ 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 @@ -261,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); } } @@ -279,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"); + } } /** @@ -307,7 +334,7 @@ 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.overwritten().name(); } else { @@ -320,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; } } @@ -333,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())); @@ -364,19 +391,21 @@ 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) { invariant(subQuery == null || explicitType == subQuery); - if (inferredType.classifier() != null) { + if (inferredType != null && inferredType.classifier() != null) { var inferredClass = (EClass) inferredType.classifier(); // check that the inferred type is assignable to the explicit type - if (explicitType instanceof MainQuery explicitMainQueryType) { + if (explicitType instanceof ConcreteMainQuery explicitMainQueryType) { invariant(explicitMainQueryType.getSource() != null, "Cannot reference a query without source"); invariant(AstUtils.checkSourceType(explicitMainQueryType.getSource(), inferredClass)); + } else if (explicitType instanceof AbstractMainQuery) { + // TODO } else { var explicitSubQueryType = (SubQuery) explicitType; var subQuerySourceType = AstUtils.inferSubQuerySourceType( @@ -389,7 +418,7 @@ private AQRTargetClass determineReferenceType( } return getTargetForQuery(explicitType); } 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 { @@ -455,7 +484,7 @@ private AQRTargetClass createRootIfNeededAndInit() { if (rootQuery != null) { root = getTargetForQuery(rootQuery); } else { - root = new AQRTargetClass(Constants.DefaultRootClassName, null, new ArrayList<>(), new ArrayList<>()); + root = new AQRTargetClass(Constants.DefaultRootClassName, false, null, new ArrayList<>(), new ArrayList<>()); targetClasses.add(root); } @@ -469,7 +498,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() 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 8005bc2d..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); 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 c5c35600..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,18 +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) { - createSuperClassScope(AstUtils.getViewType(context)); + return createSuperClassScope(AstUtils.getViewType(context)); } else if (reference == AstPackage.Literals.FEATURE__TYPE) { return createFeatureTypeScope(AstUtils.getViewType(context)); - } else if (reference == AstPackage.Literals.ABSTRACT_FEATURE__TYPE) { - return createFeatureTypeScope(AstUtils.getViewType(context)); } else { return super.getScope(context, reference); } - - return IScope.NULLSCOPE; } /** 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..6fada222 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) { @@ -227,6 +236,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 +254,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..230692b7 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 @@ -14,10 +14,13 @@ import org.jspecify.annotations.Nullable; import tools.vitruv.neojoin.Constants; import tools.vitruv.neojoin.aqr.AQRFeatureOptionsBuilder; +import tools.vitruv.neojoin.ast.AbstractMainQuery; 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 +48,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 +64,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 +101,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 +109,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 +126,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 +137,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 +155,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 +168,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 +180,7 @@ public void checkFeatureType(Feature feature) { eDataType.getName(), eDataType.getInstanceClass().getSimpleName() ), feature, - AstPackage.Literals.FEATURE__EXPRESSION + AstPackage.Literals.CONCRETE_FEATURE__EXPRESSION ); } } else { @@ -178,27 +189,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,13 +221,15 @@ private void checkSubQuery(Feature feature, @Nullable EClassifier inferredClassi } } - private void checkQueryType(Feature feature, Query explicitType, EClassifier inferredClassifier) { - if (explicitType instanceof MainQuery mainQuery) { + private void checkQueryType(ConcreteFeature feature, Query explicitType, EClassifier inferredClassifier) { + if (explicitType instanceof ConcreteMainQuery mainQuery) { if (mainQuery.getSource() != null && inferredClassifier instanceof EClass inferredClass) { if (AstUtils.checkSourceType(mainQuery.getSource(), inferredClass)) { return; } } + } else if (explicitType instanceof AbstractMainQuery) { + // TODO } else { var subQuery = (SubQuery) explicitType; @@ -236,18 +249,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 +268,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 } @@ -276,7 +289,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); @@ -296,29 +309,29 @@ public void checkAmbiguousImplicitFeatureTypes(ViewTypeDefinition viewType) { } 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 +349,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 +381,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 +397,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 +429,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 2488fe58..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 && mainQuery.getAbstractBody() == null) { - error("Query without source must have a body", mainQuery, AstPackage.Literals.QUERY__BODY); + if (mainQuery.getBody() == null) { + 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 4a42da2d..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,17 +16,18 @@ Export: Import: 'import' package=[ecore::EPackage|STRING] ('as' alias=ID)?; -MainQuery: +MainQuery: ConcreteMainQuery | AbstractMainQuery; + +ConcreteMainQuery: source=Source? - ( - 'create' root?='root'? name=ID? - ('extends' superClasses+=QualifiedName (',' superClasses+=QualifiedName)*)? - body=Body? - )|( - 'abstract' name=ID? - ('extends' superClasses+=QualifiedName (',' superClasses+=QualifiedName)*)? - abstractBody=AbstractBody - ); + 'create' root?='root'? name=ID? + ('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 @@ -49,13 +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 AbstractBody: - {AbstractBody} '{' abstractFeatures+=AbstractFeature* '}'; + {AbstractBody} '{' features+=AbstractFeature* '}'; -Feature: +ConcreteFeature: ( name=ID (':' type=[ecore::EObject|QualifiedName])? // type refers either to an EDataType or a Query @@ -89,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 dd2b82db..d953dedb 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 @@ -83,6 +83,8 @@ record Generate() implements Kind {} * @param expression expression calculating the value of the feature (not inherited) */ record Override(AQRFeature overwritten, XExpression expression) implements Kind {} + + record Abstract() implements Kind {} } /** 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 e94aba1a..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 @@ -23,6 +23,7 @@ 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; @@ -34,11 +35,13 @@ 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; @@ -48,6 +51,10 @@ public String name() { return name; } + public boolean isAbstract() { + return isAbstract; + } + public @Nullable AQRSource source() { return source; } From 9f0e433e658076ec0968b20871ee112980ba1dad Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:47:13 +0100 Subject: [PATCH 07/14] fix feature assignment check --- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 26 +++++++++-------- .../tools/vitruv/neojoin/utils/AstUtils.java | 21 ++++++++++++++ .../neojoin/validation/FeatureValidator.java | 29 +++++++++++-------- 3 files changed, 52 insertions(+), 24 deletions(-) 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 3f0e6772..caae8e6b 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 @@ -396,18 +396,20 @@ private AQRTargetClass determineReferenceType( @Nullable SubQuery subQuery ) { if (explicitType != null) { + var explicitTargetType = getTargetForQuery(explicitType); + invariant(subQuery == null || explicitType == subQuery); - if (inferredType != null && inferredType.classifier() != null) { - var inferredClass = (EClass) inferredType.classifier(); - - // check that the inferred type is assignable to the explicit type - if (explicitType instanceof ConcreteMainQuery explicitMainQueryType) { - invariant(explicitMainQueryType.getSource() != null, "Cannot reference a query without source"); - invariant(AstUtils.checkSourceType(explicitMainQueryType.getSource(), inferredClass)); - } else if (explicitType instanceof AbstractMainQuery) { - // TODO - } 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 @@ -416,7 +418,7 @@ private AQRTargetClass determineReferenceType( invariant(AstUtils.checkSourceType(subQuerySourceType, inferredClass)); } } - return getTargetForQuery(explicitType); + return explicitTargetType; } else { invariant(inferredType != null && inferredType.classifier() instanceof EClass); // includes null check if (subQuery != null) { 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 6fada222..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 @@ -183,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}. * 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 230692b7..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 @@ -14,7 +14,6 @@ import org.jspecify.annotations.Nullable; import tools.vitruv.neojoin.Constants; import tools.vitruv.neojoin.aqr.AQRFeatureOptionsBuilder; -import tools.vitruv.neojoin.ast.AbstractMainQuery; import tools.vitruv.neojoin.ast.AstPackage; import tools.vitruv.neojoin.ast.ConcreteBody; import tools.vitruv.neojoin.ast.ConcreteFeature; @@ -222,17 +221,17 @@ private void checkSubQuery(ConcreteFeature feature, @Nullable EClassifier inferr } private void checkQueryType(ConcreteFeature feature, Query explicitType, EClassifier inferredClassifier) { - if (explicitType instanceof ConcreteMainQuery mainQuery) { - if (mainQuery.getSource() != null && inferredClassifier instanceof EClass inferredClass) { - if (AstUtils.checkSourceType(mainQuery.getSource(), inferredClass)) { - return; - } + 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 if (explicitType instanceof AbstractMainQuery) { - // TODO - } else { - var subQuery = (SubQuery) explicitType; + } + if (explicitType instanceof SubQuery subQuery) { if (inferredClassifier instanceof EClass inferredClass) { var subQuerySourceType = AstUtils.inferSubQuerySourceType(subQuery, expressionHelper); if (subQuerySourceType == null) { @@ -280,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); @@ -303,6 +301,13 @@ 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)); From c2c380bc92c2a614f553f7c9bb213290b1613423 Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:23:54 +0100 Subject: [PATCH 08/14] terminology --- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 34 +++++++++---------- .../tools/vitruv/neojoin/aqr/AQRFeature.java | 6 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) 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 caae8e6b..02564d34 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 @@ -336,7 +336,7 @@ private static String getFeatureName(Feature feature, AQRFeature.Kind kind) { } else if (kind instanceof AQRFeature.Kind.Calculate) { return invariantFailed("Calculated feature must have a name: " + ((ConcreteFeature) feature).getExpression()); } else if (kind instanceof AQRFeature.Kind.Override overriding) { - return overriding.overwritten().name(); + return overriding.overridden().name(); } else { return fail(); } @@ -440,42 +440,42 @@ private TypeInfo inferType(XExpression expression) { private void applyInheritance(AQRTargetClass targetClass) { var superClassFeatures = targetClass.allSuperClasses().stream().flatMap(superClass -> superClass.features().stream()).toList(); - // update overwriting features + // update overriding features targetClass.features().replaceAll(feature -> { - var overwrittenFeatures = superClassFeatures.stream().filter(superClassFeature -> superClassFeature.name().equals(feature.name())).toList(); - if (overwrittenFeatures.isEmpty()) { + var overriddenFeatures = superClassFeatures.stream().filter(superClassFeature -> superClassFeature.name().equals(feature.name())).toList(); + if (overriddenFeatures.isEmpty()) { return feature; } - invariant(overwrittenFeatures.size() == 1); - var overwrittenFeature = overwrittenFeatures.get(0); + invariant(overriddenFeatures.size() == 1); + var overriddenFeature = overriddenFeatures.get(0); - invariant(feature.options().equals(overwrittenFeature.options()), "Overwriting features may not change modifiers"); + invariant(feature.options().equals(overriddenFeature.options()), "Overriding features may not change modifiers"); if (feature instanceof AQRFeature.Attribute attribute) { - invariant(overwrittenFeature instanceof AQRFeature.Attribute, "Attribute must overwrite attribute"); - var overwrittenAttribute = (AQRFeature.Attribute)overwrittenFeature; - invariant(overwrittenAttribute.type().equals(attribute.type()), "Type of overwriting feature must be equal to type of overwritten feature"); + 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(overwrittenFeature, feature.kind().expression())); + return attribute.withFeatureKind(new AQRFeature.Kind.Override(overriddenFeature, feature.kind().expression())); } else if (feature instanceof AQRFeature.Reference reference) { - invariant(overwrittenFeature instanceof AQRFeature.Reference, "Reference must overwrite reference"); - var overwrittenReference = (AQRFeature.Reference)overwrittenFeature; - invariant(overwrittenReference.type().equals(reference.type()), "Type of overwriting feature must be equal to type of overwritten feature"); + invariant(overriddenFeature instanceof AQRFeature.Reference, "Reference must override reference"); + var overriddenReference = (AQRFeature.Reference)overriddenFeature; + invariant(overriddenReference.type().equals(reference.type()), "Type of overriding feature must be equal to type of overridden feature"); - return reference.withFeatureKind(new AQRFeature.Kind.Override(overwrittenFeature, feature.kind().expression())); + return reference.withFeatureKind(new AQRFeature.Kind.Override(overriddenFeature, feature.kind().expression())); } else { throw new IllegalStateException("AQRFeature is a sealed interface therefore this check should be exhaustive"); } }); - // check if all inherited features are overwritten + // check if all inherited features are overridden var missingFeatures = superClassFeatures.stream().filter(superClassFeature -> !targetClass.features().stream().filter(feature -> feature.name().equals(superClassFeature.name()) && (feature.kind() instanceof AQRFeature.Kind.Override) ).findAny().isPresent()).toList(); if (!missingFeatures.isEmpty()) { - invariant(!missingFeatures.isEmpty(), "Sub classes must overwrite all inherited features"); + invariant(!missingFeatures.isEmpty(), "Sub classes must override all inherited features"); } } 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 d953dedb..9a472922 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 @@ -77,12 +77,12 @@ record Implicit(EStructuralFeature source) implements Copy {} record Generate() implements Kind {} /** - * The feature overwrites a feature in a super classs. + * The feature overrides a feature in a super classs. * - * @param overwritten overwritten feature in a super class + * @param overridden overridden feature in a super class * @param expression expression calculating the value of the feature (not inherited) */ - record Override(AQRFeature overwritten, XExpression expression) implements Kind {} + record Override(AQRFeature overridden, XExpression expression) implements Kind {} record Abstract() implements Kind {} } From 7c005350a3896222d025609c82f37e4af67a0b7c Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:55:09 +0100 Subject: [PATCH 09/14] small fixes --- .../visualization/VisualizationGenerator.java | 18 +++++++++++------- .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 11 +++-------- 2 files changed, 14 insertions(+), 15 deletions(-) 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 86ecbaad..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); @@ -204,13 +204,13 @@ private void targetClazzIfNew(EClass 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, @@ -222,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); @@ -257,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 02564d34..44f07eba 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 @@ -461,7 +461,7 @@ private void applyInheritance(AQRTargetClass targetClass) { } 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()), "Type of overriding feature must be equal to type of overridden feature"); + 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().expression())); } else { @@ -470,13 +470,8 @@ private void applyInheritance(AQRTargetClass targetClass) { }); // check if all inherited features are overridden - var missingFeatures = superClassFeatures.stream().filter(superClassFeature -> !targetClass.features().stream().filter(feature -> - feature.name().equals(superClassFeature.name()) && - (feature.kind() instanceof AQRFeature.Kind.Override) - ).findAny().isPresent()).toList(); - if (!missingFeatures.isEmpty()) { - invariant(!missingFeatures.isEmpty(), "Sub classes must override all inherited features"); - } + 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() { From 24d251f4407b4e4b3a364af1ff7ecbaa13c9bdcd Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:55:35 +0100 Subject: [PATCH 10/14] add basic CLI test for abstract classes and inheritance --- .../neojoin/cli/integration/GenerateTest.java | 2 +- .../cli/src/test/resources/queries/pizza2.nj | 21 +++++++++++++ .../src/test/resources/results/pizza2.ecore | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 lang/frontend/cli/src/test/resources/queries/pizza2.nj create mode 100644 lang/frontend/cli/src/test/resources/results/pizza2.ecore 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/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..6af42ed1 --- /dev/null +++ b/lang/frontend/cli/src/test/resources/results/pizza2.ecore @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + From 346e3e651bcc4df85d80eaa12033e33711d397dc Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:38:14 +0100 Subject: [PATCH 11/14] enable transformation of overriding features --- .../neojoin/transformation/Transformator.java | 3 ++ .../cli/integration/TransformTest.java | 2 +- .../src/test/resources/models/pizza2.ecore | 31 +++++++++++++++++++ .../cli/src/test/resources/results/pizza2.xmi | 8 +++++ .../tools/vitruv/neojoin/aqr/AQRBuilder.java | 4 +-- .../tools/vitruv/neojoin/aqr/AQRFeature.java | 11 +++++-- 6 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 lang/frontend/cli/src/test/resources/models/pizza2.ecore create mode 100644 lang/frontend/cli/src/test/resources/results/pizza2.xmi 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..534e1af5 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() @@ -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/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..6af42ed1 --- /dev/null +++ b/lang/frontend/cli/src/test/resources/models/pizza2.ecore @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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/language/src/main/java/tools/vitruv/neojoin/aqr/AQRBuilder.java b/lang/frontend/language/src/main/java/tools/vitruv/neojoin/aqr/AQRBuilder.java index 44f07eba..02b70e86 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 @@ -457,13 +457,13 @@ private void applyInheritance(AQRTargetClass targetClass) { 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().expression())); + return attribute.withFeatureKind(new AQRFeature.Kind.Override(overriddenFeature, feature.kind())); } 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().expression())); + return reference.withFeatureKind(new AQRFeature.Kind.Override(overriddenFeature, feature.kind())); } else { throw new IllegalStateException("AQRFeature is a sealed interface therefore this check should be exhaustive"); } 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 9a472922..9e24ee81 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 @@ -80,9 +80,16 @@ record Generate() implements Kind {} * The feature overrides a feature in a super classs. * * @param overridden overridden feature in a super class - * @param expression expression calculating the value of the feature (not inherited) + * @param overriding */ - record Override(AQRFeature overridden, XExpression expression) implements Kind {} + record Override(AQRFeature overridden, AQRFeature.Kind overriding) implements Kind { + + @java.lang.Override + public XExpression expression() { + return overriding.expression(); + } + + } record Abstract() implements Kind {} } From 34880b9570551d4d7a32e4891b2c49314d0836f4 Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:44:51 +0100 Subject: [PATCH 12/14] do not generate root references for abstract classes --- .../cli/src/test/resources/models/pizza2.ecore | 12 +++++------- .../cli/src/test/resources/results/pizza2.ecore | 12 +++++------- .../java/tools/vitruv/neojoin/aqr/AQRBuilder.java | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/lang/frontend/cli/src/test/resources/models/pizza2.ecore b/lang/frontend/cli/src/test/resources/models/pizza2.ecore index 6af42ed1..b42be8da 100644 --- a/lang/frontend/cli/src/test/resources/models/pizza2.ecore +++ b/lang/frontend/cli/src/test/resources/models/pizza2.ecore @@ -12,20 +12,18 @@ - - - - - + + + + diff --git a/lang/frontend/cli/src/test/resources/results/pizza2.ecore b/lang/frontend/cli/src/test/resources/results/pizza2.ecore index 6af42ed1..b42be8da 100644 --- a/lang/frontend/cli/src/test/resources/results/pizza2.ecore +++ b/lang/frontend/cli/src/test/resources/results/pizza2.ecore @@ -12,20 +12,18 @@ - - - - - + + + + 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 02b70e86..4ef975de 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 @@ -527,7 +527,7 @@ private AQRTargetClass createRootIfNeededAndInit() { */ private void populateRoot(AQRTargetClass root) { for (var target : targetClasses) { - if (target == root) { + if (target == root || target.isAbstract()) { continue; } From 0b16a76efc1b753eb9a6d2dcbbc25c8a786fb05a Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:02:35 +0100 Subject: [PATCH 13/14] fix transformation of references where instance of sub classes are referenced --- .../neojoin/transformation/TargetMap.java | 54 ++++++++++++++----- .../neojoin/transformation/Transformator.java | 4 +- 2 files changed, 43 insertions(+), 15 deletions(-) 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 534e1af5..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 @@ -307,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); } } From df2025bb3bf31a91b6b87c5b3259d4b9b0a7166e Mon Sep 17 00:00:00 2001 From: larsk21 <57503246+larsk21@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:56:15 +0100 Subject: [PATCH 14/14] fix checks for overriding features --- .../java/tools/vitruv/neojoin/aqr/AQRBuilder.java | 12 +++++++----- .../java/tools/vitruv/neojoin/aqr/AQRFeature.java | 8 ++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) 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 4ef975de..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 @@ -438,7 +438,7 @@ private TypeInfo inferType(XExpression expression) { } private void applyInheritance(AQRTargetClass targetClass) { - var superClassFeatures = targetClass.allSuperClasses().stream().flatMap(superClass -> superClass.features().stream()).toList(); + var superClassFeatures = targetClass.superClasses().stream().flatMap(superClass -> superClass.features().stream()).toList(); // update overriding features targetClass.features().replaceAll(feature -> { @@ -450,20 +450,22 @@ private void applyInheritance(AQRTargetClass targetClass) { invariant(overriddenFeatures.size() == 1); var overriddenFeature = overriddenFeatures.get(0); - invariant(feature.options().equals(overriddenFeature.options()), "Overriding features may not change modifiers"); - 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())); + 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())); + 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"); } 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 9e24ee81..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 @@ -171,6 +171,10 @@ Attribute withFeatureKind(Kind kind) { return new Attribute(name, type, kind, options); } + Attribute withOptions(Options options) { + return new Attribute(name, type, kind, options); + } + } /** @@ -197,6 +201,10 @@ Reference withFeatureKind(Kind kind) { return new Reference(name, type, kind, options); } + Reference withOptions(Options options) { + return new Reference(name, type, kind, options); + } + } }