From 2d2f77614f453a246e01a68607cd7a7617fc647a Mon Sep 17 00:00:00 2001
From: rschoene <rene.schoene@tu-dresden.de>
Date: Tue, 23 Apr 2019 14:54:51 +0200
Subject: [PATCH] Allow interleaving of AST rules and relations.

- Simplified lexer, dropping lexical state String
- Added test case for this scenario (copied from MultipleFiles)
---
 build.gradle                                  |  10 +
 src/main/jastadd/RelAst.flex                  |  64 +-
 src/main/jastadd/RelAst.parser                |  15 +-
 src/test/jastadd/multiple-mixed/Part1.relast  |   6 +
 src/test/jastadd/multiple-mixed/Part2.relast  |   4 +
 src/test/jastadd/multiple-mixed/Part3.relast  |   9 +
 .../relast/tests/MultipleMixedFiles.java      | 618 ++++++++++++++++++
 7 files changed, 674 insertions(+), 52 deletions(-)
 create mode 100644 src/test/jastadd/multiple-mixed/Part1.relast
 create mode 100644 src/test/jastadd/multiple-mixed/Part2.relast
 create mode 100644 src/test/jastadd/multiple-mixed/Part3.relast
 create mode 100644 src/test/java/org/jastadd/relast/tests/MultipleMixedFiles.java

diff --git a/build.gradle b/build.gradle
index df19329..3804964 100644
--- a/build.gradle
+++ b/build.gradle
@@ -188,6 +188,16 @@ task compileMultipleTest(type: RelastTest) {
     moreInputFiles 'src/test/jastadd/Utils.jadd'
 }
 
+task compileMultipleMixedTest(type: RelastTest) {
+    relastFiles 'src/test/jastadd/multiple-mixed/Part1.relast',
+            'src/test/jastadd/multiple-mixed/Part2.relast',
+            'src/test/jastadd/multiple-mixed/Part3.relast'
+    grammarName = 'src/test/jastadd/multiple/Multiple'
+    useJastAddNames = true
+    packageName = 'mixed.multiple.ast'
+    moreInputFiles 'src/test/jastadd/Utils.jadd'
+}
+
 task compileResolverTest(type: RelastTest) {
     relastFiles 'src/test/jastadd/resolver/Resolver.relast'
     grammarName = 'src/test/jastadd/resolver/Resolver'
diff --git a/src/main/jastadd/RelAst.flex b/src/main/jastadd/RelAst.flex
index d224f8d..68b2ebf 100644
--- a/src/main/jastadd/RelAst.flex
+++ b/src/main/jastadd/RelAst.flex
@@ -42,46 +42,30 @@ Comment = {TraditionalComment} | {EndOfLineComment}
 
 ID = [a-zA-Z$_][a-zA-Z0-9$_]*
 
-%state STRING
-
 %%
-<YYINITIAL> {
-  {WhiteSpace} { /* ignore */ }
-  {Comment}    { /* ignore */ }
-
-  "abstract"   { return sym(Terminals.ABSTRACT); }
-  "rel"        { return sym(Terminals.RELATION); }
-
-  ";"          { return sym(Terminals.SCOL); }
-  ":"          { return sym(Terminals.COL); }
-  "::="        { return sym(Terminals.ASSIGN); }
-  "*"          { return sym(Terminals.STAR); }
-  "."          { return sym(Terminals.DOT); }
-  ","          { return sym(Terminals.COMMA); }
-  "<"          { return sym(Terminals.LT); }
-  ">"          { return sym(Terminals.GT); }
-  "["          { return sym(Terminals.LBRACKET); }
-  "]"          { return sym(Terminals.RBRACKET); }
-  "/"          { return sym(Terminals.SLASH); }
-  "?"          { return sym(Terminals.QUESTION_MARK); }
-  "->"         { return sym(Terminals.RIGHT); }
-  "<->"        { return sym(Terminals.BIDIRECTIONAL); }
-
-  // ID
-  {ID}         { return sym(Terminals.ID); }
-  \"           { stringLitSb.setLength(0); yybegin(STRING); }
-  <<EOF>>      { return sym(Terminals.EOF); }
-}
-
-<STRING> {
-  \"           { yybegin(YYINITIAL); return sym(Terminals.STRING_LITERAL, stringLitSb.toString()); }
-  [^\n\r\"\\]+ { stringLitSb.append( yytext() ); }
-  \\t          { stringLitSb.append('\t'); }
-  \\n          { stringLitSb.append('\n'); }
-  \\r          { stringLitSb.append('\r'); }
-  \\\"         { stringLitSb.append('\"'); }
-  \\           { stringLitSb.append('\\'); }
-}
-
+{WhiteSpace} { /* ignore */ }
+{Comment}    { /* ignore */ }
+
+"abstract"   { return sym(Terminals.ABSTRACT); }
+"rel"        { return sym(Terminals.RELATION); }
+
+";"          { return sym(Terminals.SCOL); }
+":"          { return sym(Terminals.COL); }
+"::="        { return sym(Terminals.ASSIGN); }
+"*"          { return sym(Terminals.STAR); }
+"."          { return sym(Terminals.DOT); }
+","          { return sym(Terminals.COMMA); }
+"<"          { return sym(Terminals.LT); }
+">"          { return sym(Terminals.GT); }
+"["          { return sym(Terminals.LBRACKET); }
+"]"          { return sym(Terminals.RBRACKET); }
+"/"          { return sym(Terminals.SLASH); }
+"?"          { return sym(Terminals.QUESTION_MARK); }
+"->"         { return sym(Terminals.RIGHT); }
+"<->"        { return sym(Terminals.BIDIRECTIONAL); }
+
+// ID
+{ID}         { return sym(Terminals.ID); }
+<<EOF>>      { return sym(Terminals.EOF); }
 
 [^]            { throw new ScannerError((yyline+1) +"," + (yycolumn+1) + ": Illegal character <"+yytext()+">"); }
diff --git a/src/main/jastadd/RelAst.parser b/src/main/jastadd/RelAst.parser
index 27e0e7e..631b89c 100644
--- a/src/main/jastadd/RelAst.parser
+++ b/src/main/jastadd/RelAst.parser
@@ -1,11 +1,7 @@
 Program goal =
-  type_decls.t relations.r {: return new Program(t, r); :}
-  | STRING_LITERAL STAR {: return new Program(); :}
-  ;
-
-List type_decls =
-  /* empty */ {: return new List(); :}
-  | type_decls.l type_decl.d {: return l.add(d); :}
+  /* empty */              {: return new Program(); :}
+  | type_decl.t goal.p     {: p.getTypeDeclList().insertChild(t, 0); return p; :}
+  | relation.r goal.p      {: p.getRelationList().insertChild(r, 0); return p; :}
   ;
 
 TypeDecl type_decl =
@@ -66,11 +62,6 @@ Component component =
   | LT ID GT {: return new TokenComponent(ID, new SimpleTypeUse("String")); :}
   ;
 
-List relations =
-  /* empty */ {: return new List(); :}
-  | relations.l relation.r {: return l.add(r); :}
-  ;
-
 Relation relation =
   RELATION relation_comp.l direction relation_comp.r SCOL
   {: return new Relation(l, direction, r); :}
diff --git a/src/test/jastadd/multiple-mixed/Part1.relast b/src/test/jastadd/multiple-mixed/Part1.relast
new file mode 100644
index 0000000..04cf6ee
--- /dev/null
+++ b/src/test/jastadd/multiple-mixed/Part1.relast
@@ -0,0 +1,6 @@
+rel A.Di1  -> B;
+
+B ::= <Name>;
+
+rel A.Di2? -> B;
+rel A.Di3* -> B;
diff --git a/src/test/jastadd/multiple-mixed/Part2.relast b/src/test/jastadd/multiple-mixed/Part2.relast
new file mode 100644
index 0000000..2ce0608
--- /dev/null
+++ b/src/test/jastadd/multiple-mixed/Part2.relast
@@ -0,0 +1,4 @@
+Root ::= A* B*;
+rel A.Bi1 <-> B.Bi1;
+rel A.Bi2 <-> B.Bi2?;
+rel A.Bi3 <-> B.Bi3*;
diff --git a/src/test/jastadd/multiple-mixed/Part3.relast b/src/test/jastadd/multiple-mixed/Part3.relast
new file mode 100644
index 0000000..41c36d9
--- /dev/null
+++ b/src/test/jastadd/multiple-mixed/Part3.relast
@@ -0,0 +1,9 @@
+rel A.Bi4? <-> B.Bi4;
+rel A.Bi5? <-> B.Bi5?;
+rel A.Bi6? <-> B.Bi6*;
+
+rel A.Bi7* <-> B.Bi7;
+rel A.Bi8* <-> B.Bi8?;
+rel A.Bi9* <-> B.Bi9*;
+
+A ::= <Name>;
diff --git a/src/test/java/org/jastadd/relast/tests/MultipleMixedFiles.java b/src/test/java/org/jastadd/relast/tests/MultipleMixedFiles.java
new file mode 100644
index 0000000..975ee2a
--- /dev/null
+++ b/src/test/java/org/jastadd/relast/tests/MultipleMixedFiles.java
@@ -0,0 +1,618 @@
+package org.jastadd.relast.tests;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import mixed.multiple.ast.A;
+import mixed.multiple.ast.B;
+import mixed.multiple.ast.Root;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+class MultipleMixedFiles {
+  private Root r;
+  private A a1;
+  private A a2;
+  private A a3;
+  private B b1;
+  private B b2;
+  private B b3;
+
+  /**
+   * rel A.Di1 -> B;
+   */
+  @Test
+  void testDi1() {
+    setup();
+    a1.setDi1(b2);
+    a2.setDi1(b1);
+
+    assertSame(a1.getDi1(), b2);
+    assertSame(a2.getDi1(), b1);
+
+    a2.setDi1(b2);
+
+    assertSame(a1.getDi1(), b2);
+    assertSame(a2.getDi1(), b2);
+
+    try {
+      a3.setDi1(null);
+      fail("should throw an exception");
+    } catch (Exception e) {
+      // OK
+    }
+  }
+
+
+  /**
+   * rel A.Di2? -> B;
+   */
+  @Test
+  void testDi2() {
+    setup();
+    a1.setDi2(b2);
+    a2.setDi2(b1);
+
+    assertSame(a1.getDi2(), b2);
+    assertSame(a2.getDi2(), b1);
+
+    a2.setDi2(b2);
+
+    assertSame(a1.getDi2(), b2);
+    assertSame(a2.getDi2(), b2);
+
+    a2.clearDi2();
+
+    assertSame(a1.getDi2(), b2);
+    assertNull(a2.getDi2());
+
+    assertTrue(a1.hasDi2());
+    assertFalse(a2.hasDi2());
+    assertFalse(a3.hasDi2());
+  }
+
+
+  /**
+   * rel A.Di3* -> B;
+   */
+  @Test
+  void testDi3() {
+    setup();
+    a1.addDi3(b1);
+    a1.addDi3(b2);
+    a1.addDi3(b3);
+    a2.addDi3(b2);
+
+    assertEquals(a1.getDi3s(), Arrays.asList(b1, b2, b3));
+    assertEquals(a1.getDi3List(), Arrays.asList(b1, b2, b3));
+    assertEquals(a2.getDi3s(), Arrays.asList(b2));
+    assertEquals(a2.getDi3List(), Arrays.asList(b2));
+    assertEquals(a3.getDi3s(), Arrays.asList());
+    assertEquals(a3.getDi3List(), Arrays.asList());
+
+    a1.addDi3(b1);
+    a2.addDi3(b1);
+    a2.addDi3(b2);
+
+    assertEquals(a1.getDi3s(), Arrays.asList(b1, b2, b3, b1));
+    assertEquals(a1.getDi3List(), Arrays.asList(b1, b2, b3, b1));
+    assertEquals(a2.getDi3s(), Arrays.asList(b2, b1, b2));
+    assertEquals(a2.getDi3List(), Arrays.asList(b2, b1, b2));
+    assertEquals(a3.getDi3s(), Arrays.asList());
+    assertEquals(a3.getDi3List(), Arrays.asList());
+
+    a1.removeDi3(b1);
+    a2.removeDi3(b2);
+
+    assertEquals(a1.getDi3s(), Arrays.asList(b2, b3, b1));
+    assertEquals(a1.getDi3List(), Arrays.asList(b2, b3, b1));
+    assertEquals(a2.getDi3s(), Arrays.asList(b1, b2));
+    assertEquals(a2.getDi3List(), Arrays.asList(b1, b2));
+    assertEquals(a3.getDi3s(), Arrays.asList());
+    assertEquals(a3.getDi3List(), Arrays.asList());
+  }
+
+
+  /**
+   * rel A.Bi1 <-> B.Bi1;
+   */
+
+
+  @Test
+  void testBi11() {
+    // Init
+    setup();
+    a1.setBi1(b1);
+    a2.setBi1(b2);
+
+    // Change
+    a2.setBi1(b1);
+
+    assertNull(a1.getBi1());
+    assertSame(a2.getBi1(), b1);
+    assertSame(b1.getBi1(), a2);
+    assertNull(b2.getBi1());
+  }
+
+  @Test
+  void testBi12() {
+    // Init
+    setup();
+    a1.setBi1(b2);
+
+    // Change
+    a2.setBi1(b2);
+
+    assertNull(a1.getBi1());
+    assertSame(a2.getBi1(), b2);
+    assertNull(b1.getBi1());
+    assertSame(b2.getBi1(), a2);
+  }
+
+
+  /**
+   * rel A.Bi2 <-> B.Bi2?;
+   */
+
+  @Test
+  void testBi21() {
+    // Init
+    setup();
+    a1.setBi2(b1);
+    a2.setBi2(b2);
+
+    // Change
+    a2.setBi2(b1);
+
+    assertNull(a1.getBi2());
+    assertSame(a2.getBi2(), b1);
+    assertSame(b1.getBi2(), a2);
+    assertNull(b2.getBi2());
+  }
+
+  @Test
+  void testBi22() {
+    // Init
+    setup();
+    a1.setBi2(b2);
+
+    // Change
+    a2.setBi2(b2);
+
+    assertNull(a1.getBi2());
+    assertSame(a2.getBi2(), b2);
+    assertNull(b1.getBi2());
+    assertSame(b2.getBi2(), a2);
+  }
+
+
+  /**
+   * rel A.Bi3 <-> B.Bi3*;
+   */
+  @Test
+  void testBi3() {
+    setup();
+    a2.setBi3(b2);
+
+    assertNull(a1.getBi3());
+    assertSame(a2.getBi3(), b2);
+    assertEquals(b1.getBi3s(), Arrays.asList());
+    assertEquals(b1.getBi3List(), Arrays.asList());
+    assertEquals(b2.getBi3s(), Arrays.asList(a2));
+    assertEquals(b2.getBi3List(), Arrays.asList(a2));
+    assertEquals(b3.getBi3s(), Arrays.asList());
+    assertEquals(b3.getBi3List(), Arrays.asList());
+
+    a2.setBi3(b3);
+
+    assertNull(a1.getBi3());
+    assertSame(a2.getBi3(), b3);
+    assertEquals(b1.getBi3s(), Arrays.asList());
+    assertEquals(b1.getBi3List(), Arrays.asList());
+    assertEquals(b2.getBi3s(), Arrays.asList());
+    assertEquals(b2.getBi3List(), Arrays.asList());
+    assertEquals(b3.getBi3s(), Arrays.asList(a2));
+    assertEquals(b3.getBi3List(), Arrays.asList(a2));
+
+    a1.setBi3(b3);
+    a3.setBi3(b3);
+
+    assertSame(a1.getBi3(), b3);
+    assertSame(a2.getBi3(), b3);
+    assertSame(a3.getBi3(), b3);
+    assertEquals(b1.getBi3s(), Arrays.asList());
+    assertEquals(b1.getBi3List(), Arrays.asList());
+    assertEquals(b2.getBi3s(), Arrays.asList());
+    assertEquals(b2.getBi3List(), Arrays.asList());
+    assertEquals(b3.getBi3s(), Arrays.asList(a2, a1, a3));
+    assertEquals(b3.getBi3List(), Arrays.asList(a2, a1, a3));
+
+    a2.setBi3(b1);
+
+    assertSame(a1.getBi3(), b3);
+    assertSame(a2.getBi3(), b1);
+    assertSame(a3.getBi3(), b3);
+    assertEquals(b1.getBi3s(), Arrays.asList(a2));
+    assertEquals(b1.getBi3List(), Arrays.asList(a2));
+    assertEquals(b2.getBi3s(), Arrays.asList());
+    assertEquals(b2.getBi3List(), Arrays.asList());
+    assertEquals(b3.getBi3s(), Arrays.asList(a1, a3));
+    assertEquals(b3.getBi3List(), Arrays.asList(a1, a3));
+
+    try {
+      a2.setBi3(null);
+      fail("should throw an exception");
+    } catch (Exception e) {
+      // OK
+    }
+  }
+
+
+  /**
+   * rel A.Bi4? <-> B.Bi4;
+   */
+  @Test
+  void testBi41() {
+    // Init
+    setup();
+    a1.setBi4(b1);
+    a2.setBi4(b2);
+
+    // Change
+    a2.setBi4(b1);
+
+    assertNull(a1.getBi4());
+    assertSame(a2.getBi4(), b1);
+    assertSame(b1.getBi4(), a2);
+    assertNull(b2.getBi4());
+  }
+
+  @Test
+  void testBi42() {
+    // Init
+    setup();
+    a1.setBi4(b2);
+
+    // Change
+    a2.setBi4(b2);
+
+    assertNull(a1.getBi4());
+    assertSame(a2.getBi4(), b2);
+    assertNull(b1.getBi4());
+    assertSame(b2.getBi4(), a2);
+  }
+
+
+  /**
+   * rel A.Bi5? <-> B.Bi5?;
+   */
+  @Test
+  void testBi51() {
+    // Init
+    setup();
+    a1.setBi5(b1);
+    a2.setBi5(b2);
+
+    // Change
+    a2.setBi5(b1);
+
+    assertNull(a1.getBi5());
+    assertSame(a2.getBi5(), b1);
+    assertSame(b1.getBi5(), a2);
+    assertNull(b2.getBi5());
+  }
+
+  @Test
+  void testBi52() {
+    // Init
+    setup();
+    a1.setBi5(b2);
+
+    // Change
+    a2.setBi5(b2);
+
+    assertNull(a1.getBi5());
+    assertSame(a2.getBi5(), b2);
+    assertNull(b1.getBi5());
+    assertSame(b2.getBi5(), a2);
+  }
+
+
+  /**
+   * rel A.Bi6? <-> B.Bi6*;
+   */
+  @Test
+  void testBi6() {
+    setup();
+    a2.setBi6(b2);
+
+    assertNull(a1.getBi6());
+    assertSame(a2.getBi6(), b2);
+    assertEquals(b1.getBi6s(), Arrays.asList());
+    assertEquals(b1.getBi6List(), Arrays.asList());
+    assertEquals(b2.getBi6s(), Arrays.asList(a2));
+    assertEquals(b2.getBi6List(), Arrays.asList(a2));
+    assertEquals(b3.getBi6s(), Arrays.asList());
+    assertEquals(b3.getBi6List(), Arrays.asList());
+
+    a2.setBi6(b3);
+
+    assertNull(a1.getBi6());
+    assertSame(a2.getBi6(), b3);
+    assertEquals(b1.getBi6s(), Arrays.asList());
+    assertEquals(b1.getBi6List(), Arrays.asList());
+    assertEquals(b2.getBi6s(), Arrays.asList());
+    assertEquals(b2.getBi6List(), Arrays.asList());
+    assertEquals(b3.getBi6s(), Arrays.asList(a2));
+    assertEquals(b3.getBi6List(), Arrays.asList(a2));
+
+    a1.setBi6(b3);
+    a3.setBi6(b3);
+
+    assertSame(a1.getBi6(), b3);
+    assertSame(a2.getBi6(), b3);
+    assertSame(a3.getBi6(), b3);
+    assertEquals(b1.getBi6s(), Arrays.asList());
+    assertEquals(b1.getBi6List(), Arrays.asList());
+    assertEquals(b2.getBi6s(), Arrays.asList());
+    assertEquals(b2.getBi6List(), Arrays.asList());
+    assertEquals(b3.getBi6s(), Arrays.asList(a2, a1, a3));
+    assertEquals(b3.getBi6List(), Arrays.asList(a2, a1, a3));
+
+    a2.setBi6(b1);
+
+    assertSame(a1.getBi6(), b3);
+    assertSame(a2.getBi6(), b1);
+    assertSame(a3.getBi6(), b3);
+    assertEquals(b1.getBi6s(), Arrays.asList(a2));
+    assertEquals(b1.getBi6List(), Arrays.asList(a2));
+    assertEquals(b2.getBi6s(), Arrays.asList());
+    assertEquals(b2.getBi6List(), Arrays.asList());
+    assertEquals(b3.getBi6s(), Arrays.asList(a1, a3));
+    assertEquals(b3.getBi6List(), Arrays.asList(a1, a3));
+
+    a2.clearBi6();
+
+    assertSame(a1.getBi6(), b3);
+    assertNull(a2.getBi6());
+    assertSame(a3.getBi6(), b3);
+    assertEquals(b1.getBi6s(), Arrays.asList());
+    assertEquals(b1.getBi6List(), Arrays.asList());
+    assertEquals(b2.getBi6s(), Arrays.asList());
+    assertEquals(b2.getBi6List(), Arrays.asList());
+    assertEquals(b3.getBi6s(), Arrays.asList(a1, a3));
+    assertEquals(b3.getBi6List(), Arrays.asList(a1, a3));
+
+    assertTrue(a1.hasBi6());
+    assertFalse(a2.hasBi6());
+    assertTrue(a3.hasBi6());
+  }
+
+
+  /**
+   * rel A.Bi7* <-> B.Bi7;
+   */
+  @Test
+  void testBi7() {
+    setup();
+    a2.addBi7(b2);
+
+    assertEquals(a1.getBi7s(), Arrays.asList());
+    assertEquals(a1.getBi7List(), Arrays.asList());
+    assertEquals(a2.getBi7s(), Arrays.asList(b2));
+    assertEquals(a2.getBi7List(), Arrays.asList(b2));
+    assertNull(b1.getBi7());
+    assertSame(b2.getBi7(), a2);
+    assertNull(b3.getBi7());
+
+    a2.addBi7(b3);
+    a1.addBi7(b2);
+
+    assertEquals(a1.getBi7s(), Arrays.asList(b2));
+    assertEquals(a1.getBi7List(), Arrays.asList(b2));
+    assertEquals(a2.getBi7s(), Arrays.asList(b3));
+    assertEquals(a2.getBi7List(), Arrays.asList(b3));
+    assertNull(b1.getBi7());
+    assertSame(b2.getBi7(), a1);
+    assertSame(b3.getBi7(), a2);
+
+    a1.addBi7(b1);
+
+    assertEquals(a1.getBi7s(), Arrays.asList(b2, b1));
+    assertEquals(a1.getBi7List(), Arrays.asList(b2, b1));
+    assertEquals(a2.getBi7s(), Arrays.asList(b3));
+    assertEquals(a2.getBi7List(), Arrays.asList(b3));
+    assertSame(b1.getBi7(), a1);
+    assertSame(b2.getBi7(), a1);
+    assertSame(b3.getBi7(), a2);
+
+    a1.addBi7(b1);
+
+    assertEquals(a1.getBi7s(), Arrays.asList(b2, b1));
+    assertEquals(a1.getBi7List(), Arrays.asList(b2, b1));
+    assertEquals(a2.getBi7s(), Arrays.asList(b3));
+    assertEquals(a2.getBi7List(), Arrays.asList(b3));
+    assertSame(b1.getBi7(), a1);
+    assertSame(b2.getBi7(), a1);
+    assertSame(b3.getBi7(), a2);
+
+    a1.removeBi7(b1);
+
+    assertEquals(a1.getBi7s(), Arrays.asList(b2));
+    assertEquals(a1.getBi7List(), Arrays.asList(b2));
+    assertEquals(a2.getBi7s(), Arrays.asList(b3));
+    assertEquals(a2.getBi7List(), Arrays.asList(b3));
+    assertNull(b1.getBi7());
+    assertSame(b2.getBi7(), a1);
+    assertSame(b3.getBi7(), a2);
+  }
+
+
+  /**
+   * rel A.Bi8* <-> B.Bi8?;
+   */
+  @Test
+  void testBi8() {
+    setup();
+    a2.addBi8(b2);
+
+    assertEquals(a1.getBi8s(), Arrays.asList());
+    assertEquals(a1.getBi8List(), Arrays.asList());
+    assertEquals(a2.getBi8s(), Arrays.asList(b2));
+    assertEquals(a2.getBi8List(), Arrays.asList(b2));
+    assertNull(b1.getBi8());
+    assertSame(b2.getBi8(), a2);
+    assertNull(b3.getBi8());
+
+    a2.addBi8(b3);
+    a1.addBi8(b2);
+
+    assertEquals(a1.getBi8s(), Arrays.asList(b2));
+    assertEquals(a1.getBi8List(), Arrays.asList(b2));
+    assertEquals(a2.getBi8s(), Arrays.asList(b3));
+    assertEquals(a2.getBi8List(), Arrays.asList(b3));
+    assertNull(b1.getBi8());
+    assertSame(b2.getBi8(), a1);
+    assertSame(b3.getBi8(), a2);
+
+    a1.addBi8(b1);
+
+    assertEquals(a1.getBi8s(), Arrays.asList(b2, b1));
+    assertEquals(a1.getBi8List(), Arrays.asList(b2, b1));
+    assertEquals(a2.getBi8s(), Arrays.asList(b3));
+    assertEquals(a2.getBi8List(), Arrays.asList(b3));
+    assertSame(b1.getBi8(), a1);
+    assertSame(b2.getBi8(), a1);
+    assertSame(b3.getBi8(), a2);
+
+    a1.addBi8(b1);
+
+    assertEquals(a1.getBi8s(), Arrays.asList(b2, b1));
+    assertEquals(a1.getBi8List(), Arrays.asList(b2, b1));
+    assertEquals(a2.getBi8s(), Arrays.asList(b3));
+    assertEquals(a2.getBi8List(), Arrays.asList(b3));
+    assertSame(b1.getBi8(), a1);
+    assertSame(b2.getBi8(), a1);
+    assertSame(b3.getBi8(), a2);
+
+    a1.removeBi8(b1);
+
+    assertEquals(a1.getBi8s(), Arrays.asList(b2));
+    assertEquals(a1.getBi8List(), Arrays.asList(b2));
+    assertEquals(a2.getBi8s(), Arrays.asList(b3));
+    assertEquals(a2.getBi8List(), Arrays.asList(b3));
+    assertNull(b1.getBi8());
+    assertSame(b2.getBi8(), a1);
+    assertSame(b3.getBi8(), a2);
+  }
+
+
+  /**
+   * rel A.Bi9* <-> B.Bi9*;
+   */
+  @Test
+  void testBi9() {
+    setup();
+    a1.addBi9(b1);
+    a1.addBi9(b2);
+
+    assertEquals(a1.getBi9s(), Arrays.asList(b1, b2));
+    assertEquals(a1.getBi9List(), Arrays.asList(b1, b2));
+    assertEquals(a2.getBi9s(), Arrays.asList());
+    assertEquals(a2.getBi9List(), Arrays.asList());
+    assertEquals(a3.getBi9s(), Arrays.asList());
+    assertEquals(a3.getBi9List(), Arrays.asList());
+    assertEquals(b1.getBi9s(), Arrays.asList(a1));
+    assertEquals(b1.getBi9List(), Arrays.asList(a1));
+    assertEquals(b2.getBi9s(), Arrays.asList(a1));
+    assertEquals(b2.getBi9List(), Arrays.asList(a1));
+    assertEquals(b3.getBi9s(), Arrays.asList());
+    assertEquals(b3.getBi9List(), Arrays.asList());
+
+    b3.addBi9(a1);
+    b3.addBi9(a3);
+    b3.addBi9(a1);
+
+    assertEquals(a1.getBi9s(), Arrays.asList(b1, b2, b3, b3));
+    assertEquals(a1.getBi9List(), Arrays.asList(b1, b2, b3, b3));
+    assertEquals(a2.getBi9s(), Arrays.asList());
+    assertEquals(a2.getBi9List(), Arrays.asList());
+    assertEquals(a3.getBi9s(), Arrays.asList(b3));
+    assertEquals(a3.getBi9List(), Arrays.asList(b3));
+    assertEquals(b1.getBi9s(), Arrays.asList(a1));
+    assertEquals(b1.getBi9List(), Arrays.asList(a1));
+    assertEquals(b2.getBi9s(), Arrays.asList(a1));
+    assertEquals(b2.getBi9List(), Arrays.asList(a1));
+    assertEquals(b3.getBi9s(), Arrays.asList(a1, a3, a1));
+    assertEquals(b3.getBi9List(), Arrays.asList(a1, a3, a1));
+
+    b3.removeBi9(a1);
+
+    assertEquals(a1.getBi9s(), Arrays.asList(b1, b2, b3));
+    assertEquals(a1.getBi9List(), Arrays.asList(b1, b2, b3));
+    assertEquals(a2.getBi9s(), Arrays.asList());
+    assertEquals(a2.getBi9List(), Arrays.asList());
+    assertEquals(a3.getBi9s(), Arrays.asList(b3));
+    assertEquals(a3.getBi9List(), Arrays.asList(b3));
+    assertEquals(b1.getBi9s(), Arrays.asList(a1));
+    assertEquals(b1.getBi9List(), Arrays.asList(a1));
+    assertEquals(b2.getBi9s(), Arrays.asList(a1));
+    assertEquals(b2.getBi9List(), Arrays.asList(a1));
+    assertEquals(b3.getBi9s(), Arrays.asList(a3, a1));
+    assertEquals(b3.getBi9List(), Arrays.asList(a3, a1));
+  }
+
+
+  @Test
+  void testImmutableList() {
+    setup();
+
+    a1.addDi3(b1);
+    a1.addDi3(b2);
+    try {
+      a1.getDi3s().add(b3);
+      fail("should throw an exception");
+    } catch (Exception e) {
+      // OK
+    }
+
+    a1.addBi7(b1);
+    a1.addBi7(b2);
+    try {
+      a1.getBi7s().add(b3);
+      fail("should throw an exception");
+    } catch (Exception e) {
+      // OK
+    }
+
+    a1.addBi9(b1);
+    a1.addBi9(b2);
+    try {
+      a1.getBi9s().add(b3);
+      fail("should throw an exception");
+    } catch (Exception e) {
+      // OK
+    }
+  }
+
+  @BeforeEach
+  void setup() {
+    r = new Root();
+    a1 = new A("a1");
+    a2 = new A("a2");
+    a3 = new A("a3");
+    b1 = new B("b1");
+    b2 = new B("b2");
+    b3 = new B("b3");
+
+    r.addA(a1);
+    r.addA(a2);
+    r.addA(a3);
+    r.addB(b1);
+    r.addB(b2);
+    r.addB(b3);
+  }
+}
-- 
GitLab