Browse Source

WIP: Embedding of versions

Joeri Exelmans 2 years ago
parent
commit
55d89b1045

+ 7 - 1
src/onion/composite_delta.ts

@@ -45,6 +45,12 @@ export class CompositeDelta implements Delta {
       deltas: this.deltas.map(d => d.serialize()),
     }
   }
+
+  *iterPrimitiveDeltas(): Iterable<Delta> {
+    for (const d of this.deltas) {
+      yield* d.iterPrimitiveDeltas();
+    }
+  }
 }
 
 // A "registry" of composite deltas.
@@ -89,7 +95,7 @@ export class CompositeLevel {
         const compositeConflict = this.containedBy.get(conflict);
         if (compositeConflict === undefined) {
           // We used to treat this as an error, however, it's possible that a conflicting delta simply isn't part yet of a composite delta...
-          
+
           // throw new Error("Assertion failed: cannot find composite of " + conflict.getDescription());
         } else {
           if (!conflicts.includes(compositeConflict)) {

+ 3 - 0
src/onion/delta.ts

@@ -20,6 +20,9 @@ export interface Delta {
 
   // Result must survive a JSON round-trip.
   serialize(): any;
+
+  // Get all primitive deltas that this delta consists of.
+  iterPrimitiveDeltas(): Iterable<Delta>;
 }
 
 export function isConflicting(a: Delta, b: Delta) {

+ 61 - 0
src/onion/parser.test.ts

@@ -0,0 +1,61 @@
+import {Delta} from "./delta";
+import {Version, VersionRegistry} from "./version";
+import {CompositeLevel} from "./composite_delta";
+import {mockUuid} from "./test_helpers";
+
+type ParseFunction = (cs: Version, parentCorr: Version) => Version;
+
+function getParentLink(cs: Version, parentCorr: Version): [Version,Delta] {
+  const parentCsEmbedding = parentCorr.getEmbedded("cs");
+  if (parentCsEmbedding === undefined) {
+    throw new Error("Parent correspondence model has no 'cs' embedding!");
+  }
+  const csParentLink = cs.parents.find(([parentCs]) =>  parentCsEmbedding.embedded === parentCs);
+  if (csParentLink === undefined) {
+    throw new Error("Parent correspondence model does not embed parent of 'cs' model!");
+  }
+  return csParentLink;
+}
+
+interface Parser {
+  parse(cs: Version, parentCorr: Version): Version;
+}
+
+// A parser that creates an AS-node for every CS-node, with a Corr-node in between.
+class TrivialParser implements Parser {
+  readonly registry: VersionRegistry;
+  readonly compositeLvl: CompositeLevel;
+
+  constructor(registry: VersionRegistry, compositeLvl: CompositeLevel) {
+    this.registry = registry;
+    this.compositeLvl = compositeLvl;
+  }
+
+  parse(cs: Version, parentCorr: Version): Version {
+    const [csParent, csDelta] = getParentLink(cs, parentCorr);
+
+    const emptyDelta = this.compositeLvl.createComposite([]);
+
+    return this.registry.createVersion(parentCorr, emptyDelta);
+  }
+}
+
+describe("Parser", () => {
+  it("Parse CS creation", () => {
+    const registry = new VersionRegistry();
+    const compositeLvl = new CompositeLevel();
+    const parser = new TrivialParser(registry, compositeLvl);
+    const getUuid = mockUuid();
+
+    // const csInitial = 
+
+    // // L0:
+    // const l0csCreation = new NodeCreation(getUuid());
+
+    // // L1:
+    // const l1csCreation = compositeLvl.createComposite([l0csCreation]);
+
+    // // L2:
+
+  });
+});

+ 16 - 0
src/onion/primitive_delta.ts

@@ -65,6 +65,10 @@ export class NodeCreation implements PrimitiveDelta {
       id: this.id.value,
     }
   }
+
+  *iterPrimitiveDeltas(): Iterable<Delta> {
+    yield this;
+  }
 }
 
 export class NodeDeletion implements PrimitiveDelta {
@@ -265,6 +269,10 @@ export class NodeDeletion implements PrimitiveDelta {
       afterIncomingEdges: this.afterIncomingEdges.map(d => d.hash.toString('base64')),
     };
   }
+
+  *iterPrimitiveDeltas(): Iterable<Delta> {
+    yield this;
+  }
 }
 
 // Target of an edge can be: another node, nothing (edge doesn't exist) or a value (i.e., string, number or boolean)
@@ -488,6 +496,10 @@ export class EdgeCreation implements PrimitiveDelta {
       target: this.target.serialize(),
     };
   }
+
+  *iterPrimitiveDeltas(): Iterable<Delta> {
+    yield this;
+  }
 }
 
 export class EdgeUpdate implements PrimitiveDelta {
@@ -582,5 +594,9 @@ export class EdgeUpdate implements PrimitiveDelta {
       }
     }
   }
+
+  *iterPrimitiveDeltas(): Iterable<Delta> {
+    yield this;
+  }
 }
 

+ 10 - 2
src/onion/version.test.ts

@@ -4,6 +4,8 @@ import {
   Version,
   initialVersion,
   VersionRegistry,
+  embed,
+  overrideDeltas,
 } from "./version";
 
 import {
@@ -27,6 +29,7 @@ import {
 
 import {
   assert,
+  assertThrows,
 } from "../util/assert";
 
 describe("Version", () => {
@@ -269,8 +272,13 @@ describe("Version", () => {
 
       const csCorrDelLvl2 = lvl2.createComposite([corrDelLvl1, csDel1Lvl1]);
 
-      const corrV1 = registry.createVersion(initialVersion, csCorrLinkLvl2);
-      const corrV2 = registry.createVersion(corrV1, csCorrDelLvl2);
+      const corrV1 = registry.createVersion(initialVersion, csCorrLinkLvl2, embed(["cs", csV1, overrideDeltas()]));
+      const corrV2 = registry.createVersion(corrV1, csCorrDelLvl2, embed(["cs", csV2, overrideDeltas( [csDel, csDel1] )]));
+
+      assertThrows(() => {
+        // same as above, but without the overridden deltas:
+        registry.createVersion(corrV1, csCorrDelLvl2, embed(["cs", csV2, overrideDeltas()])); // should throw
+      }, "should not be able to create a version that embeds csV2 without overriding csDel");
     });
   });
 });

+ 78 - 9
src/onion/version.ts

@@ -1,6 +1,7 @@
 import {inspect} from "util"; // NodeJS library
 import { Buffer } from "buffer"; // NodeJS library
 import {findDFS} from "../util/dfs";
+import {bufferXOR} from "./buffer_xor";
 
 import * as _ from "lodash";
 
@@ -11,7 +12,6 @@ import {
   iterConflicts,
 } from "./delta";
 
-import {bufferXOR} from "./buffer_xor";
 
 // Wrapper class, each time a version is embedded into another version, an instance of this class is contructed.
 // More precisely, if a version is embedded by N other versions, then N instances of EmbeddedVersion are constructed.
@@ -25,6 +25,16 @@ export interface EmbeddedVersion {
 
 export type EmbeddedVersionMap = Map<string, EmbeddedVersion>;
 
+// Just a shorthand
+export function embed(...triples: [string, Version, Map<Delta,Delta>][]): EmbeddedVersionMap {
+  return new Map(triples.map(([id, version, overriddenDeltas]) => [id, {embedded: version, overriddenDeltas}]));
+}
+
+// Just a shorthand
+export function overrideDeltas(...tuples: [Delta,Delta][]): Map<Delta,Delta> {
+  return new Map(tuples);
+}
+
 
 // A typed link from one version to another.
 // There are two types: 'p' (parent) and 'c' (child)
@@ -35,6 +45,7 @@ type VersionLink = [('p'|'c'), Delta];
 export class Version {
   readonly parents: Array<[Version, Delta]>;
   readonly children: Array<[Version, Delta]> = [];
+  private readonly embeddings: EmbeddedVersionMap; // do not access directly - use getEmbedded() instead
 
   // Unique ID of the version - XOR of all delta hashes - guarantees that Versions with equal (unordered) sets of Deltas have the same ID.
   readonly hash: Buffer;
@@ -42,11 +53,12 @@ export class Version {
   readonly size: number;
 
   // DO NOT USE constructor directly - instead use VersionRegistry.createVersion.
-  constructor(parents: Array<[Version, Delta]>, hash: Buffer, size: number) {
+  constructor(parents: Array<[Version, Delta]>, embeddings: EmbeddedVersionMap, hash: Buffer, size: number) {
     this.parents = parents;
     for (const [parent,delta] of parents) {
       parent.children.push([this, delta]);
     }
+    this.embeddings = embeddings;
     this.hash = hash;
     this.size = size;
   }
@@ -95,12 +107,34 @@ export class Version {
 
     return findDFS<Version,VersionLink>(this, otherVersion, getNeighbors);
   }
+
+  getEmbedded(id: string): EmbeddedVersion | undefined {
+    return this.embeddings.get(id);
+  }
 }
 
-// The initial, empty version.
 const initialHash = Buffer.alloc(32); // all zeros
-export const initialVersion = new Version([], initialHash, 0);
 
+export class InitialVersion extends Version {
+  private static instance: InitialVersion = new InitialVersion(); // singleton pattern
+  private static embedded: EmbeddedVersion = {embedded: InitialVersion.instance, overriddenDeltas: new Map()};
+
+  private constructor() {
+    super([], new Map(), initialHash, 0);
+  }
+
+  static getInstance(): InitialVersion {
+    return InitialVersion.instance;
+  }
+
+  // override: we pretend that the initial version has all other empty models (also represented by the initial version) embedded into it.
+  getEmbedded(id: string): EmbeddedVersion | undefined {
+    return InitialVersion.embedded;
+  }
+}
+
+// The initial, empty version.
+export const initialVersion = InitialVersion.getInstance();
 
 export class VersionRegistry {
   // Maps version ID (as string, because a Buffer cannot be a map key) to Version
@@ -127,23 +161,58 @@ export class VersionRegistry {
 
   // Pre-condition 1: all of the dependencies of delta must exist in parent.
   // Pre-condition 2: delta must be non-conflicting with any delta in parent.
-  createVersion(parent: Version, delta: Delta): Version {
+  // Pre-condition 3: this version and its parent must embed the same types of "guest" versions (e.g., "cs", "as", ...)
+  // Pre-condition 4: at least one of the parents of every embedded version must be embedded by the parent of this version.
+  // Pre-condition 5: if the to-be-created ("host") version embeds other ("guest") versions,
+  //   then all of the guest versions' deltas must exist in the host version, or be explicitly overridden.
+  // Idempotent
+  createVersion(parent: Version, delta: Delta, embeddings: EmbeddedVersionMap = new Map()): Version {
     // Check pre-condition 1:
     const missingDependency = iterMissingDependencies(delta, parent).next().value;
     if (missingDependency !== undefined) {
       throw new Error("Missing dependency: " + missingDependency.toString());
     }
+
     // Check pre-condition 2:
     const conflictsWith = iterConflicts(delta, parent).next().value;
     if (conflictsWith !== undefined) {
-      console.log(delta);
       throw new Error("Delta " + delta.toString() + " conflicts with " + conflictsWith.toString());
     }
-    return this.createVersionUnsafe(parent, delta);
+
+    const primitives = [...delta.iterPrimitiveDeltas()];
+
+    for (const [guestId, {embedded, overriddenDeltas}] of embeddings.entries()) {
+      const parentEmbedding = parent.getEmbedded(guestId);
+      // Check pre-condition 3:
+      if (parentEmbedding === undefined) {
+        throw new Error("Attempted to create illegal version: parent version does not embed '" + guestId + "'");
+      }
+      const {embedded: parentEmbedded} = parentEmbedding;
+      // Check pre-condition 4:
+      const found = embedded.parents.find(([parentOfEmbedded]) => (parentOfEmbedded === parentEmbedded));
+      if (found === undefined) {
+        throw new Error("Attempted to create illegal version that embeds a guest, whose parent is not embedded by the parent of the to-be-created version.");
+      }
+      // Check pre-condition 5:
+      // console.log("primitives:", primitives);
+      const [parentOfEmbedded, embeddedDelta] = found;
+      for (const embeddedPrimitive of embeddedDelta.iterPrimitiveDeltas()) {
+        // console.log("embeddedPrimitive:", embeddedPrimitive);
+        const haveOriginal = primitives.includes(embeddedPrimitive);
+        const overridden = overriddenDeltas.get(embeddedPrimitive);
+        const haveOverridden = ((overridden !== undefined) && primitives.includes(overridden));
+        // console.log(haveOriginal, haveOverridden)
+        if (!haveOriginal && !haveOverridden || (haveOriginal && haveOverridden)) { // logical XOR
+          throw new Error("Attempt to create illegal version: must contain all deltas (some possibly overridden) of all embedded versions.");
+        }
+      }
+    }
+    return this.createVersionUnsafe(parent, delta, embeddings);
   }
 
   // Faster than createVersion, but does not check pre-conditions.
-  createVersionUnsafe(parent: Version, delta: Delta): Version {
+  // Idempotent
+  createVersionUnsafe(parent: Version, delta: Delta, embeddings: EmbeddedVersionMap = new Map()): Version {
     const newHash = bufferXOR(parent.hash, delta.getHash());
     const existingVersion = this.lookupOptional(newHash);
     if (existingVersion !== undefined) {
@@ -156,7 +225,7 @@ export class VersionRegistry {
       }
       return existingVersion;
     } else {
-      const version = new Version([[parent, delta]], newHash, parent.size + 1);
+      const version = new Version([[parent, delta]], embeddings, newHash, parent.size + 1);
       this.putVersion(newHash, version);
       return version;
     }

+ 14 - 0
src/util/assert.ts

@@ -9,3 +9,17 @@ export function assert(expression: boolean, msg: string) {
     throw new Error(msg);
   }
 }
+
+export function assertThrows(callback, msg) {
+  let threw = false;
+  try {
+    callback()
+  }
+  catch (e) {
+    threw = true;
+  }
+
+  if (!threw) {
+    throw new Error(msg);
+  }
+}