diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 84ac997..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: csharp -solution: Mingle.sln -dist: trusty -mono: none -dotnet: 2.0.0 -before_install: - - curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg - - sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg - - sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main" > /etc/apt/sources.list.d/dotnetdev.list' -script: - - dotnet restore - - dotnet build \ No newline at end of file diff --git a/README.md b/README.md index bff005b..40cfde7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,20 @@ | branch | build status | |---|---| -| `master` | [![Build Status](https://travis-ci.org/Stephanvs/Mingle.svg?branch=master)](https://travis-ci.org/Stephanvs/Mingle) | -| `dev` | [![Build Status](https://travis-ci.org/Stephanvs/Mingle.svg?branch=dev)](https://travis-ci.org/Stephanvs/Mingle) | \ No newline at end of file +| `master` | [![Build Status](https://dev.azure.com/Stephanvs/Mingle/_apis/build/status/Stephanvs.Mingle?branchName=master)](https://dev.azure.com/Stephanvs/Mingle/_build/latest?definitionId=1) | +| `dev` | [![Build Status](https://dev.azure.com/Stephanvs/Mingle/_apis/build/status/Stephanvs.Mingle?branchName=dev)](https://dev.azure.com/Stephanvs/Mingle/_build/latest?definitionId=1) | + +# What is a CRDT? + +Conflict-free, Coordination-free, Commutative, or Convergent datatypes, CRDT's are usually formally described as "join semi-lattices". Mathematical jargon aside, CRDT's track causality for modifications to your data. Because of this, time becomes less relevant, and coordination becomes unnecessary to get accurate values for your data. + +# Want to learn more? + +Here are some resources that may help you understand further. + +- Strong Eventual Consistency and Conflict-free Replicated Data Types + - A good introduction to the concept of CRDTs: http://research.microsoft.com/apps/video/default.aspx?id=153540&r=1 +- A comprehensive study of Convergent and Commutative Replicated Data Types + - A survey with references for several popular CRDTs: http://hal.inria.fr/docs/00/55/55/88/PDF/techreport.pdf +- Efficient State-based CRDTs by Delta-Mutation + - Talk: https://www.youtube.com/watch?v=y_ewFP-lgyM + - Paper: http://arxiv.org/pdf/1410.2803v1.pdf \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cc30f67..122f832 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,5 +12,16 @@ variables: steps: - script: dotnet build --configuration $(buildConfiguration) displayName: 'dotnet build $(buildConfiguration)' -- script: dotnet test - displayName: 'dotnet test' \ No newline at end of file + +- task: DotNetCoreCLI@2 + displayName: 'run unit tests' + inputs: + command: test + projects: 'test/**/*.csproj' + arguments: '--configuration $(buildConfiguration) --collect "Code coverage"' + +- script: dotnet publish --output $(Build.ArtifactStagingDirectory) + displayName: 'dotnet publish' + +- task: PublishBuildArtifacts@1 + displayName: 'publish build artifacts' \ No newline at end of file diff --git a/src/Attributes.cs b/src/Attributes.cs new file mode 100644 index 0000000..577dfff --- /dev/null +++ b/src/Attributes.cs @@ -0,0 +1,5 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Mingle.Tests")] \ No newline at end of file diff --git a/src/Core/Cmd.cs b/src/Core/Cmd.cs index 8c47bc3..1f5a281 100644 --- a/src/Core/Cmd.cs +++ b/src/Core/Cmd.cs @@ -9,106 +9,100 @@ public sealed class Before : BeforeAfter { } public sealed class After : BeforeAfter { } - public interface Cmd + public interface Cmd : IComparable { } public sealed class Let : Record, Cmd { - private readonly Var _x; - private readonly Expr _expr; + public readonly Var X; + public readonly Expr Expr; public Let(Var x, Expr expr) { - _x = x; - _expr = expr; + X = x; + Expr = expr; } - public Var X => _x; - - public Expr Expr => _expr; + public int CompareTo(Cmd other) + => RecordType.Compare(this, other as Let); } public sealed class Assign : Record, Cmd { - private readonly Expr _expr; - private readonly Val _value; + public readonly Expr Expr; + public readonly Val Value; public Assign(Expr expr, Val value) { - _expr = expr; - _value = value; + Expr = expr; + Value = value; } - public Expr Expr => _expr; - - public Val Value => _value; + public int CompareTo(Cmd other) + => RecordType.Compare(this, other as Assign); } public sealed class Insert : Record, Cmd { - private readonly Expr _expr; - private readonly Val _value; + public readonly Expr Expr; + public readonly Val Value; public Insert(Expr expr, Val value) { - _expr = expr; - _value = value; + Expr = expr; + Value = value; } - public Expr Expr => _expr; - - public Val Value => _value; + public int CompareTo(Cmd other) + => RecordType.Compare(this, other as Insert); } public sealed class Delete : Record, Cmd { - private readonly Expr _expr; + public readonly Expr Expr; public Delete(Expr expr) { - _expr = expr; + Expr = expr; } - public Expr Expr => _expr; + public int CompareTo(Cmd other) + => RecordType.Compare(this, other as Delete); } public sealed class MoveVertical : Record, Cmd { - private readonly Expr _moveExpr; - private readonly Expr _targetExpr; - private readonly BeforeAfter _beforeAfter; + public readonly Expr MoveExpr; + public readonly Expr TargetExpr; + public readonly BeforeAfter BeforeAfter; public MoveVertical( Expr moveExpr, Expr targetExpr, BeforeAfter beforeAfter) { - _moveExpr = moveExpr; - _targetExpr = targetExpr; - _beforeAfter = beforeAfter; + MoveExpr = moveExpr; + TargetExpr = targetExpr; + BeforeAfter = beforeAfter; } - public Expr MoveExpr => _moveExpr; - - public Expr TargetExpr => _targetExpr; - - public BeforeAfter BeforeAfter => _beforeAfter; + public int CompareTo(Cmd other) + => RecordType.Compare(this, other as MoveVertical); } public sealed class Sequence : Record, Cmd { - private readonly Cmd _cmd1; - private readonly Cmd _cmd2; + public readonly Cmd Cmd1; + public readonly Cmd Cmd2; public Sequence(Cmd cmd1, Cmd cmd2) { - _cmd1 = cmd1; - _cmd2 = cmd2; + Cmd1 = cmd1; + Cmd2 = cmd2; } - public Cmd Cmd1 => _cmd1; - - public Cmd Cmd2 => _cmd2; + public int CompareTo(Cmd other) + => RecordType.Compare(this, other as Sequence); } } \ No newline at end of file diff --git a/src/Core/Cursor.cs b/src/Core/Cursor.cs index e940c90..1f25bcf 100644 --- a/src/Core/Cursor.cs +++ b/src/Core/Cursor.cs @@ -1,30 +1,28 @@ using System; +using System.Collections.Generic; using LanguageExt; +using static LanguageExt.Prelude; namespace Mingle { - public /* immutable */ class Cursor + public /* immutable */ sealed class Cursor : Record { - private Cursor(Lst keys, Key finalKey) + public readonly Lst Keys; + public readonly Key FinalKey; + + internal Cursor(Lst keys, Key finalKey) { Keys = keys; FinalKey = finalKey; } - public Lst Keys { get; } - - public Key FinalKey { get; } - public Cursor Append(Func tag, Key newFinalKey) - { - var branchTag = tag(FinalKey); - return new Cursor(Keys.Add(branchTag), newFinalKey); - } + => new Cursor(Keys.Add(tag(FinalKey)), newFinalKey); public Cursor.IView View() - { - return new Leaf(FinalKey); - } + => match(Keys, + () => new Leaf(FinalKey), + (k1, kn) => new Branch(k1, new Cursor(kn.Freeze(), FinalKey))); public static Cursor Doc() => WithFinalKey(new DocK()); @@ -32,36 +30,33 @@ public static Cursor Doc() public static Cursor WithFinalKey(Key finalKey) => new Cursor(Lst.Empty, finalKey); + internal Cursor Copy(Lst? keys = null, Key finalKey = null) + => new Cursor(keys: keys ?? Keys, finalKey: finalKey ?? FinalKey); + public interface IView { } - public class Leaf : Record, IView + public sealed class Leaf : Record, IView { - private readonly Key _finalKey; + public readonly Key FinalKey; public Leaf(Key finalKey) { - _finalKey = finalKey; + FinalKey = finalKey; } - - public Key FinalKey => _finalKey; } - public class Branch : Record, IView + public sealed class Branch : Record, IView { - private readonly BranchTag _head; - private readonly Cursor _tail; + public readonly BranchTag Head; + public readonly Cursor Tail; public Branch(BranchTag head, Cursor tail) { - _head = head; - _tail = tail; + Head = head; + Tail = tail; } - - public BranchTag Head => _head; - - public Cursor Tail => _tail; } } } \ No newline at end of file diff --git a/src/Core/Expr.cs b/src/Core/Expr.cs index b200cfd..422c2d1 100644 --- a/src/Core/Expr.cs +++ b/src/Core/Expr.cs @@ -13,53 +13,43 @@ public sealed class Doc : Record, Expr public sealed class Var : Record, Expr { - private readonly string _name; + public readonly string Name; public Var(string name) { - _name = name; + Name = name; } - - public string Name => _name; } public sealed class DownField : Record, Expr { - private readonly Expr _expr; - private readonly string _key; + public readonly Expr Expr; + public readonly string Key; public DownField(Expr expr, string key) { - _expr = expr; - _key = key; + Expr = expr; + Key = key; } - - public Expr Expr => _expr; - - public string Key => _key; } public sealed class Iter : Record, Expr { - private readonly Expr _expr; + public readonly Expr Expr; public Iter(Expr expr) { - _expr = expr; + Expr = expr; } - - public Expr Expr => _expr; } public sealed class Next : Record, Expr { - private readonly Expr _expr; + public readonly Expr Expr; public Next(Expr expr) { - _expr = expr; + Expr = expr; } - - public Expr Expr => _expr; } } \ No newline at end of file diff --git a/src/Core/Id.cs b/src/Core/Id.cs index 898d672..dc957f1 100644 --- a/src/Core/Id.cs +++ b/src/Core/Id.cs @@ -1,27 +1,23 @@ -using System; +using System; +using System.Numerics; using LanguageExt; +using LanguageExt.ClassInstances; +using LanguageExt.TypeClasses; namespace Mingle { - public /* immutable */ sealed class Id : Record, IComparable, IComparable + public /* immutable */ sealed class Id : Record { - private readonly bigint _opsCounter; - private readonly ReplicaId _replicaId; + public readonly BigInteger OpsCounter; + public readonly ReplicaId ReplicaId; - public Id(bigint opsCounter, ReplicaId replicaId) + public Id(BigInteger opsCounter, string replicaId) + : this(opsCounter, new ReplicaId(replicaId)) {} + + public Id(BigInteger opsCounter, ReplicaId replicaId) { - _opsCounter = opsCounter; - _replicaId = replicaId; + OpsCounter = opsCounter; + ReplicaId = replicaId; } - - public bigint OpsCounter => _opsCounter; - - public ReplicaId ReplicaId => _replicaId; - - public override int CompareTo(Id other) - => RecordType.Compare(this, other); - - public int CompareTo(object obj) - => (obj is Id id) ? CompareTo(id) : 0; } } \ No newline at end of file diff --git a/src/Core/Key.cs b/src/Core/Key.cs index 3b859d1..413a328 100644 --- a/src/Core/Key.cs +++ b/src/Core/Key.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using LanguageExt; namespace Mingle @@ -7,27 +8,28 @@ public interface Key : IComparable { } - public class DocK : Record, Key + public sealed class DocK : Record, Key { public int CompareTo(object obj) => 0; } - public class HeadK : Record, Key + public sealed class HeadK : Record, Key { public int CompareTo(object obj) => 0; } public sealed class IdK : Record, Key { - private readonly Id _id; + public readonly Id Id; + + public IdK(BigInteger opsCounter, string replicaId) + : this(new Id(opsCounter, replicaId)) {} public IdK(Id id) { - _id = id; + Id = id; } - public Id Id => _id; - public int CompareTo(object obj) { return obj is IdK id @@ -38,15 +40,13 @@ public int CompareTo(object obj) public sealed class StrK : Record, Key { - private readonly string _str; + public readonly string Str; public StrK(string str) { - _str = str; + Str = str; } - public string Str => _str; - public int CompareTo(object obj) { return obj is StrK k diff --git a/src/Core/ListRef.cs b/src/Core/ListRef.cs index 773ffb9..a6cd3d6 100644 --- a/src/Core/ListRef.cs +++ b/src/Core/ListRef.cs @@ -1,8 +1,9 @@ using System; +using LanguageExt; namespace Mingle { - public abstract class ListRef + public abstract class ListRef : Record, IComparable { public static ListRef FromKey(Key key) { @@ -13,6 +14,8 @@ public static ListRef FromKey(Key key) default: return new TailR(); } } + + public abstract int CompareTo(object other); } public abstract class KeyRef : ListRef @@ -26,22 +29,36 @@ public Key ToKey() } throw new InvalidOperationException( - $"Cannot convert {this.GetType().Name} using method '{nameof(ToKey)}' to {nameof(Key)}." - + " Are you illegaly subclassing {nameof(KeyRef)}?"); + $"Cannot convert {this.GetType().Name} using method '{nameof(ToKey)}' to {nameof(Key)}. " + + $"Are you illegaly subclassing {nameof(KeyRef)}?"); } + + public override int CompareTo(object other) + => RecordType.Compare(this, other as KeyRef); } - public class IdR : KeyRef + public sealed class IdR : KeyRef { + public readonly Id Id; + public IdR(Id id) { Id = id; } - public Id Id { get; } + public override int CompareTo(object other) + => RecordType.Compare(this, other as IdR); } - public sealed class HeadR : KeyRef { } + public sealed class HeadR : KeyRef + { + public override int CompareTo(object other) + => RecordType.Compare(this, other as HeadR); + } - public sealed class TailR : ListRef { } + public sealed class TailR : ListRef + { + public override int CompareTo(object other) + => RecordType.Compare(this, other as TailR); + } } \ No newline at end of file diff --git a/src/Core/Mutation.cs b/src/Core/Mutation.cs index 162ae29..415a18c 100644 --- a/src/Core/Mutation.cs +++ b/src/Core/Mutation.cs @@ -9,26 +9,22 @@ public interface Mutation public sealed class AssignM : Record, Mutation { - private readonly Val _value; + public readonly Val Value; public AssignM(Val value) { - _value = value; + Value = value; } - - public Val Value => _value; } public sealed class InsertM : Record, Mutation { - private readonly Val _value; + public readonly Val Value; public InsertM(Val value) { - _value = value; + Value = value; } - - public Val Value => _value; } public sealed class DeleteM : Record, Mutation @@ -37,17 +33,13 @@ public sealed class DeleteM : Record, Mutation public sealed class MoveVerticalM : Record, Mutation { - private readonly Cursor _targetCursor; - private readonly BeforeAfter _aboveBelow; + public readonly Cursor TargetCursor; + public readonly BeforeAfter AboveBelow; public MoveVerticalM(Cursor targetCursor, BeforeAfter aboveBelow) { - _targetCursor = targetCursor; - _aboveBelow = aboveBelow; + TargetCursor = targetCursor; + AboveBelow = aboveBelow; } - - public Cursor TargetCursor => _targetCursor; - - public BeforeAfter AboveBelow => _aboveBelow; } } \ No newline at end of file diff --git a/src/Core/Node.cs b/src/Core/Node.cs index 578a045..e5bf7f5 100644 --- a/src/Core/Node.cs +++ b/src/Core/Node.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using LanguageExt; using static LanguageExt.Prelude; namespace Mingle { - public abstract class Node : Record + public abstract class Node : Record, IComparable { public static Node EmptyMap => new MapNode( @@ -18,7 +19,7 @@ public static Node EmptyList LanguageExt.Map.Empty, LanguageExt.Map>.Empty, LanguageExt.Map.Empty, - LanguageExt.Map>.Empty); + LanguageExt.Map>.Empty); public static Node EmptyReg => new RegNode(LanguageExt.Map.Empty); @@ -39,7 +40,7 @@ public Cursor Next(Cursor cursor) var k1 = keyRef.ToKey(); var cur1 = Cursor.WithFinalKey(k1); // NEXT2 - if (!GetPres(k1).IsEmpty) { return cur1; } + if (GetPres(k1).Any()) { return cur1; } // NEXT3 else { return Next(cur1); } } @@ -50,10 +51,11 @@ public Cursor Next(Cursor cursor) // NEXT4 case Cursor.Branch b: { - // FindChild(b.Head); - // var cur2 = ; - // return cursor.Copy(finalKey: cur2.FinalKey); - throw new InvalidOperationException(); + return FindChild(b.Head).Fold(cursor, (s, child) => + { + var cur2 = child.Next(s); + return cursor.Copy(finalKey: cur2.FinalKey); + }); } default: @@ -74,6 +76,31 @@ public Lst Values(Cursor cursor) throw new NotImplementedException(); } + /** If this node is a list: Save the order of item in the list. + * Don't overwrite if it already exists. Why? Assumed it would be overwritten + * and user1, user2 and user3 do an op concurrently. When user2's op arrives + * at user1, the order is reset and both ops are redone. Each time an op is + * applied, the order is saved. Therefore when the user2's op is applied, the + * order is saved. That's the order after user1's op was applied. When now + * user3's op arrives and the order is reset, the order we reset to should be + * the order before all three ops were applied. But it's not, since user2's op + * has overwritten the order. Therefore don't overwrite. */ + private Node SaveOrder(Operation operation) + { + switch (this) + { + case ListNode ln: + { + if (ln.OrderArchive.Exists((k, v) => k == operation.Id.OpsCounter && v.IsEmpty)) + { + return ln.Copy(orderArchive: ln.OrderArchive.Add(operation.Id.OpsCounter, ln.Order)); + } + return this; + } + default: return this; + } + } + public Node ApplyOp(Operation op, Replica replica) { var view = op.Cursor.View(); @@ -85,11 +112,21 @@ public Node ApplyOp(Operation op, Replica replica) { case ListNode ln: { - IEnumerable ConcurrentOpsSince(bigint count) + IEnumerable ConcurrentOpsSince(BigInteger count) { var allOps = replica.GeneratedOps.Append(replica.ReceivedOps); - throw new NotImplementedException(); + foreach (var o in allOps) + { + if ((replica.ProcessedOps.Contains(o.Id) && + (o.Mutation is InsertM || o.Mutation is DeleteM || o.Mutation is MoveVerticalM) && + o.Id == op.Id) && + o.Id.OpsCounter >= count && + this.FindChild(new RegT(o.Cursor.FinalKey)).IsSome) + { + yield return o; + } + } } var concurrentOps = ConcurrentOpsSince(op.Id.OpsCounter); @@ -113,7 +150,7 @@ IEnumerable ConcurrentOpsSince(bigint count) var newerOrders = ln.OrderArchive.Filter((k, v) => k >= op.Id.OpsCounter); // restore the order - // TODO: + // TODO: // val ctx1 = // if (newerOrders.nonEmpty) ln.copy(order = newerOrders.minBy { // case (c, _) => c @@ -194,7 +231,45 @@ private Node ApplyAtLeaf(Operation op, Replica replica) } case InsertM ins: { - throw new NotImplementedException(); + var prevRef = ListRef.FromKey(k); + var nextRef = GetNextRef(prevRef); + switch (nextRef) + { + // INSERT2 + // INSERT 2 handles the case of multiple replicas concurrently + // inserting list elements at the same position, and uses the + // ordering relation < on Lamport timestamps to consistently + // determine the insertion point. + case IdR nextId: + { + // Normally, the nextId is lower, since it was already inserted and + // newer ops have a higher id. NextId may be only higher, if two + // insert operations are concurrent the one the other one - with the + // higher id - was already inserted. When that's the case, insert + // the new op after the concurrent one with higher id. This way, when + // inserted at the same place, the op whose user id is higher, + // comes always fist. + if (op.Id > nextId.Id) + return ApplyAtLeaf(op.Copy(cursor: Cursor.WithFinalKey(new IdK(nextId.Id))), replica); + else goto default; + } + // INSERT1 + // INSERT1 performs the insertion by manipulating the linked + // list structure. + default: + { + var idRef = new IdR(op.Id); + // the ID of the inserted node will be the ID of the operation + var ctx1 = ApplyAtLeaf( + op.Copy(cursor: Cursor.WithFinalKey(new IdK(op.Id)), + mutation: new AssignM(ins.Value)), + replica); + var ctx2 = ctx1.SaveOrder(op); + return ctx2 + .SetNextRef(prevRef, idRef) + .SetNextRef(idRef, nextRef); + } + } } case MoveVerticalM mv: { @@ -227,22 +302,30 @@ private Node AddId(TypeTag tag, Id id, Mutation mutation) switch (mutation) { case DeleteM del: return this; - default: { + default: + { var pres = GetPres(tag.Key); var presP = pres.AddOrUpdate(id); - return SetPres(tag.Key, presP); + return SetPres(tag.Key, presP); } } } private (Node, Set) ClearElem(Set deps, Key key) { - throw new NotImplementedException(); + var (ctx1, pres1) = ClearAny(deps, key); + var pres2 = ctx1.GetPres(key); + var pres3 = pres1.Append(pres2).Subtract(deps); + return (ctx1.SetPres(key, pres3), pres3); } private (Node, Set) ClearAny(Set deps, Key key) { - throw new NotImplementedException(); + var ctx0 = this; + var (ctx1, pres1) = ctx0.Clear(deps, new MapT(key)); + var (ctx2, pres2) = ctx1.Clear(deps, new ListT(key)); + var (ctx3, pres3) = ctx2.Clear(deps, new RegT(key)); + return (ctx3, pres1.Append(pres2).Append(pres3)); } private (Node, Set) Clear(Set deps, TypeTag tag) @@ -336,6 +419,15 @@ Map> RemoveOrUpdate(Map> map, K k, Set val) } } + private ListRef GetPreviousRef(ListRef @ref) + { + switch (this) + { + case ListNode ln: { return ln.Order.ContainsKey(@ref) ? ln.Order[@ref] : new HeadR(); } + default: return new HeadR(); + } + } + private ListRef GetNextRef(ListRef @ref) { switch (this) @@ -344,25 +436,35 @@ private ListRef GetNextRef(ListRef @ref) default: return new TailR(); } } + + private Node SetNextRef(ListRef src, ListRef dst) + { + switch (this) + { + case ListNode ln: + { + return ln.Copy(order: ln.Order.AddOrUpdate(src, dst)); + } + default: return this; + } + } + + public abstract int CompareTo(object obj); } public abstract class BranchNode : Node { - private readonly Map _children; - private readonly Map> _presSets; + public readonly Map Children; + public readonly Map> PresSets; public BranchNode( Map children, Map> presSets) { - _children = children; - _presSets = presSets; + Children = children; + PresSets = presSets; } - public Map Children => _children; - - public Map> PresSets => _presSets; - public abstract BranchNode WithChildren(Map children); public abstract BranchNode WithPresSets(Map> presSets); @@ -383,6 +485,18 @@ public override BranchNode WithChildren(Map children) public override BranchNode WithPresSets(Map> presSets) => Copy(presSets: presSets); + public override bool Equals(object other) + => RecordType.EqualityTyped(this, other as MapNode); + + public override int GetHashCode() + => RecordType.Hash(this); + + public override int CompareTo(object other) + => RecordType.Compare(this, other as MapNode); + + public override int CompareTo(Node other) + => RecordType.Compare(this, other as MapNode); + // public override bool Equals(object obj) // { // if (obj is MapNode mn) @@ -412,73 +526,76 @@ private MapNode Copy( public class ListNode : BranchNode { - private readonly Map _order; + public readonly Map Order; + /** The tests cannot converge, since the orderArchive of two replicas is + * always different. Therefore don't compare the orderArchive. + */ [OptOutOfEq] [OptOutOfOrd] - private readonly Map> _orderArchive; + public readonly Map> OrderArchive; public ListNode( Map children, Map> presSets, Map order, - Map> orderArchive) + Map> orderArchive) : base(children, presSets) { - _order = order; - _orderArchive = orderArchive; + Order = order; + OrderArchive = orderArchive; } - public Map Order => _order; - - public Map> OrderArchive => _orderArchive; - public override BranchNode WithChildren(Map children) - { - throw new NotImplementedException(); - } + => Copy(children: children); public override BranchNode WithPresSets(Map> presSets) - { - throw new NotImplementedException(); - } + => Copy(presSets: presSets); - // /** The tests cannot converge, since the orderArchive of two replicas is - // * always different. Therefore don't compare the orderArchive. - // */ - // public override bool Equals(object obj) - // { - // if (obj is ListNode ln) - // { - // return ln.Children == Children - // && ln.PresSets == PresSets - // && ln.Order == Order; - // } + public override bool Equals(object other) + => RecordType.EqualityTyped(this, other as ListNode); - // return base.Equals(obj); - // } + public override int GetHashCode() + => RecordType.Hash(this); - // public override int GetHashCode() - // { - // throw new NotImplementedException(); - // } + public override int CompareTo(object other) + => RecordType.Compare(this, other as ListNode); + + internal ListNode Copy( + Map>? presSets = null, + Map? children = null, + Map? order = null, + Map>? orderArchive = null) + => new ListNode( + children: children ?? Children, + presSets: presSets ?? PresSets, + order: order ?? Order, + orderArchive: orderArchive ?? OrderArchive + ); } public class RegNode : Node { - private readonly Map _regValues; + public readonly Map RegValues; public RegNode(Map regValues) { - _regValues = regValues; + RegValues = regValues; } - public Map RegValues => _regValues; - public Lst Values() => new Lst(RegValues.Values); public RegNode Copy(Map? regValues = null) - => new RegNode(regValues ?? _regValues); + => new RegNode(regValues ?? RegValues); + + public override bool Equals(object other) + => RecordType.EqualityTyped(this, other as RegNode); + + public override int GetHashCode() + => RecordType.Hash(this); + + public override int CompareTo(object other) + => RecordType.Compare(this, other as RegNode); } } \ No newline at end of file diff --git a/src/Core/Operation.cs b/src/Core/Operation.cs index edb09a2..a6d723d 100644 --- a/src/Core/Operation.cs +++ b/src/Core/Operation.cs @@ -4,10 +4,10 @@ namespace Mingle { public sealed /* immutable */ class Operation : Record { - private readonly Id _id; - private readonly Set _deps; - private readonly Cursor _cursor; - private readonly Mutation _mutation; + public readonly Id Id; + public readonly Set Deps; + public readonly Cursor Cursor; + public readonly Mutation Mutation; public Operation( Id id, @@ -15,20 +15,12 @@ public Operation( Cursor cursor, Mutation mutation) { - _id = id; - _deps = deps; - _cursor = cursor; - _mutation = mutation; + Id = id; + Deps = deps; + Cursor = cursor; + Mutation = mutation; } - public Id Id => _id; - - public Set Deps => _deps; - - public Cursor Cursor => _cursor; - - public Mutation Mutation => _mutation; - public Operation Copy( Id id = null, Set? deps = null, diff --git a/src/Core/Replica.cs b/src/Core/Replica.cs index 831e914..b60001d 100644 --- a/src/Core/Replica.cs +++ b/src/Core/Replica.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Numerics; using LanguageExt; using static LanguageExt.Prelude; @@ -7,9 +8,17 @@ namespace Mingle { public /* immutable */ sealed class Replica : Record { + public readonly ReplicaId ReplicaId; + public readonly BigInteger OpsCounter; + public readonly Node Document; + public readonly Map Variables; + public readonly Set ProcessedOps; + public readonly Lst GeneratedOps; + public readonly Lst ReceivedOps; + private Replica( ReplicaId replicaId, - bigint opsCounter, + BigInteger opsCounter, Node document, Map variables, Set processedOps, @@ -25,20 +34,6 @@ private Replica( ReceivedOps = receivedOps; } - public ReplicaId ReplicaId { get; } - - public bigint OpsCounter { get; } - - public Node Document { get; } - - public Map Variables { get; } - - public Set ProcessedOps { get; } - - public Lst GeneratedOps { get; } - - public Lst ReceivedOps { get; } - public Id CurrentId => new Id(OpsCounter, ReplicaId); @@ -58,7 +53,7 @@ public Replica ApplyLocal(Operation operation) public Replica ApplyRemote() => match(FindApplicableRemoteOp(), Some: op => Copy( - opsCounter: bigint.Max(OpsCounter, op.Id.OpsCounter), + opsCounter: BigInteger.Max(OpsCounter, op.Id.OpsCounter), document: Document.ApplyOp(op, this), processedOps: ProcessedOps.Add(op.Id)).ApplyRemote(), None: () => this); @@ -67,11 +62,9 @@ public Replica ApplyRemoteOps(Lst ops) => Copy(receivedOps: ops.AddRange(ReceivedOps)).ApplyRemote(); private Option FindApplicableRemoteOp() - { - return ReceivedOps.Find(op + => ReceivedOps.Find(op => !ProcessedOps.Contains(op.Id) && op.Deps.IsSubsetOf(ProcessedOps)); - } public Replica IncrementCounter() => Copy(opsCounter: OpsCounter + 1); @@ -107,7 +100,7 @@ Cursor Go(Expr ex, Lst> fs) // .Some(cur => cur) // .None(Cursor.Doc()); // Variables[var] - // NOTE: This is not correct, I think I should iterate over each element in the `Variables` map, and foreach element lookup the + // NOTE: This is not correct, I think I should iterate over each element in the `Variables` map, and foreach element lookup the // return match (Variables, // Some: cur => cur, // None: Cursor.Doc()); @@ -116,7 +109,7 @@ Cursor Go(Expr ex, Lst> fs) case DownField df: { - Cursor Func(Cursor c) + Cursor f(Cursor c) { switch (c.FinalKey) { @@ -131,21 +124,21 @@ Cursor Func(Cursor c) } } - return Go(df.Expr, fs.Add(Func)); + return Go(df.Expr, fs.Insert(0, f)); } case Iter it: { - Cursor Func(Cursor c) + Cursor f(Cursor c) => c.Append(k => new ListT(k), new HeadK()); - return Go(it.Expr, fs.Add(Func)); + return Go(it.Expr, fs.Insert(0, f)); } case Next next: { - Func func = Document.Next; - return Go(next.Expr, fs.Add(func)); + Func f = Document.Next; + return Go(next.Expr, fs.Insert(0, f)); } default: @@ -213,10 +206,13 @@ public static Replica ApplyCmds(Replica replica, Lst cmds) return replica; }); + public static Replica Empty(string replicaId) + => Empty(new ReplicaId(replicaId)); + public static Replica Empty(ReplicaId replicaId) => new Replica( replicaId, - opsCounter: bigint.Zero, + opsCounter: BigInteger.Zero, document: Node.EmptyMap, variables: Map(), processedOps: Set(), @@ -225,7 +221,7 @@ public static Replica Empty(ReplicaId replicaId) private Replica Copy( ReplicaId replicaId = null, - bigint? opsCounter = null, + BigInteger? opsCounter = null, Node document = null, Map? variables = null, Set? processedOps = null, diff --git a/src/Core/ReplicaId.cs b/src/Core/ReplicaId.cs index d510908..4a5b214 100644 --- a/src/Core/ReplicaId.cs +++ b/src/Core/ReplicaId.cs @@ -16,6 +16,6 @@ public ReplicaId(string id) { } - public override string ToString() => $"Id:{Value}"; + public override string ToString() => $"ReplicaId({Value})"; } } \ No newline at end of file diff --git a/src/Core/Syntax/CmdOps.cs b/src/Core/Syntax/CmdOps.cs index 231d6b4..3e93232 100644 --- a/src/Core/Syntax/CmdOps.cs +++ b/src/Core/Syntax/CmdOps.cs @@ -4,10 +4,7 @@ namespace Mingle { public static class CmdOps { - public static Cmd Assign(this Expr expr, string value) - => new Assign(expr, new Str(value)); - - public static Cmd Assign(this Expr expr, Val value) - => new Assign(expr, value); + public static Cmd Append(this Cmd cmd1, Cmd cmd2) + => new Sequence(cmd1, cmd2); } } \ No newline at end of file diff --git a/src/Core/Syntax/ExprOps.cs b/src/Core/Syntax/ExprOps.cs index 1403b29..ba15245 100644 --- a/src/Core/Syntax/ExprOps.cs +++ b/src/Core/Syntax/ExprOps.cs @@ -4,7 +4,31 @@ namespace Mingle { public static class ExprOps { + public static Cmd Assign(this Expr expr, string value) + => new Assign(expr, new Str(value)); + + public static Cmd Assign(this Expr expr, Val value) + => new Assign(expr, value); + public static DownField DownField(this Expr expr, string key) => new DownField(expr, key); + + public static Insert Insert(this Expr expr, Val value) + => new Insert(expr, value); + + public static Insert Insert(this Expr expr, string value) + => new Insert(expr, new Str(value)); + + public static Insert Insert(this Expr expr, bool value) + => new Insert(expr, value ? (Val)new True() : (Val)new False()); + + public static Delete Delete(this Expr expr) + => new Delete(expr); + + public static Iter Iter(this Expr expr) + => new Iter(expr); + + public static Next Next(this Expr expr) + => new Next(expr); } } \ No newline at end of file diff --git a/src/Core/TypeTag.cs b/src/Core/TypeTag.cs index 4c7e756..cd97f53 100644 --- a/src/Core/TypeTag.cs +++ b/src/Core/TypeTag.cs @@ -3,7 +3,7 @@ namespace Mingle { - public interface TypeTag : IComparable + public interface TypeTag { Key Key { get; } } @@ -12,54 +12,66 @@ public interface BranchTag : TypeTag { } - public sealed class MapT : Record, BranchTag + public sealed class MapT : Record, BranchTag, IComparable { + public readonly Key _key; + public MapT(Key key) { - Key = key; + _key = key; } - public Key Key { get; } + public Key Key => _key; + // public int CompareTo(object obj) + // { + // return (obj is MapT o) + // ? o.Key.CompareTo(this.Key) + // : 0; + // } public int CompareTo(object obj) - { - return (obj is MapT o) - ? o.Key.CompareTo(this.Key) - : 0; - } + => RecordType.Compare(this, obj as MapT); } - public sealed class ListT : Record, BranchTag + public sealed class ListT : Record, BranchTag, IComparable { + public readonly Key _key; + public ListT(Key key) { - Key = key; + _key = key; } - public Key Key { get; } + public Key Key => _key; + // public int CompareTo(object obj) + // { + // return (obj is ListT o) + // ? o.Key.CompareTo(this.Key) + // : 0; + // } public int CompareTo(object obj) - { - return (obj is ListT o) - ? o.Key.CompareTo(this.Key) - : 0; - } + => RecordType.Compare(this, obj as ListT); } - public sealed class RegT : Record, TypeTag + public sealed class RegT : Record, TypeTag, IComparable { + public readonly Key _key; + public RegT(Key key) { - Key = key; + _key = key; } - public Key Key { get; } + public Key Key => _key; + // public int CompareTo(object obj) + // { + // return (obj is RegT o) + // ? o.Key.CompareTo(this.Key) + // : 0; + // } public int CompareTo(object obj) - { - return (obj is RegT o) - ? o.Key.CompareTo(this.Key) - : 0; - } + => RecordType.Compare(this, obj as RegT); } } \ No newline at end of file diff --git a/src/Core/Val.cs b/src/Core/Val.cs index b12e914..0640d73 100644 --- a/src/Core/Val.cs +++ b/src/Core/Val.cs @@ -1,3 +1,4 @@ +using System.Numerics; using LanguageExt; namespace Mingle @@ -8,25 +9,41 @@ public interface BranchVal : Val { } public sealed class Num : Record, LeafVal { - private readonly bigint _value; + public readonly BigInteger Value; - public Num(bigint value) + public Num(BigInteger value) { - _value = value; + Value = value; } - - public bigint Value => _value; } public sealed class Str : Record, LeafVal { - private readonly string _value; + public readonly string Value; public Str(string value) { - _value = value; + Value = value; } + } + + public sealed class True : Record, LeafVal + { + } + + public sealed class False : Record, LeafVal + { + } - public string Value => _value; + public sealed class Null : Record, LeafVal + { + } + + public sealed class EmptyList : Record, BranchVal + { + } + + public sealed class EmptyMap : Record, BranchVal + { } } \ No newline at end of file diff --git a/src/Mingle.csproj b/src/Mingle.csproj index ece8b25..7215bf8 100644 --- a/src/Mingle.csproj +++ b/src/Mingle.csproj @@ -4,13 +4,9 @@ Mingle - + - - - - \ No newline at end of file diff --git a/test/Given_a_Doc/When_DownField.cs b/test/Given_a_Doc/When_DownField.cs index d1f4ace..4c53182 100644 --- a/test/Given_a_Doc/When_DownField.cs +++ b/test/Given_a_Doc/When_DownField.cs @@ -18,7 +18,6 @@ public class When_DownField => Subject.Key.Should().Be("key"); public static Doc Document; - public static DownField Subject; } } \ No newline at end of file diff --git a/test/Given_a_Replica/When_empty.cs b/test/Given_a_Replica/When_empty.cs index 6df6b5c..916cd25 100644 --- a/test/Given_a_Replica/When_empty.cs +++ b/test/Given_a_Replica/When_empty.cs @@ -1,3 +1,4 @@ +using System.Numerics; using FluentAssertions; using LanguageExt; using Machine.Specifications; @@ -14,7 +15,7 @@ public class When_empty => Subject.ReplicaId.Should().Be(ReplicaId.New("1234")); It should_start_with_opsCounter_of_zero = () - => Subject.CurrentId.OpsCounter.ShouldBeEquivalentTo(bigint.Zero); + => Subject.CurrentId.OpsCounter.ShouldBeEquivalentTo(BigInteger.Zero); static Replica Subject; } diff --git a/test/Given_a_Replica/When_incrementing_counter.cs b/test/Given_a_Replica/When_incrementing_counter.cs index 588606f..3d4435b 100644 --- a/test/Given_a_Replica/When_incrementing_counter.cs +++ b/test/Given_a_Replica/When_incrementing_counter.cs @@ -1,3 +1,4 @@ +using System.Numerics; using FluentAssertions; using LanguageExt; using Machine.Specifications; @@ -17,7 +18,7 @@ public class When_incrementing_counter => Subject.Should().NotBeSameAs(Empty); It should_have_increased_the_counter = () - => Subject.OpsCounter.ShouldBeEquivalentTo(bigint.One); + => Subject.OpsCounter.ShouldBeEquivalentTo(BigInteger.One); static Replica Empty; static Replica Subject; diff --git a/test/Mingle.Tests.csproj b/test/Mingle.Tests.csproj index ee16ebd..28e6cfc 100644 --- a/test/Mingle.Tests.csproj +++ b/test/Mingle.Tests.csproj @@ -1,11 +1,11 @@ - netcoreapp2.0 + netcoreapp2.1 false - + @@ -15,6 +15,5 @@ - \ No newline at end of file diff --git a/test/ReplicaSpecs.cs b/test/ReplicaSpecs.cs new file mode 100644 index 0000000..ce613f6 --- /dev/null +++ b/test/ReplicaSpecs.cs @@ -0,0 +1,93 @@ +using Xunit; +using LanguageExt; +using static LanguageExt.Prelude; + +namespace Mingle.Tests +{ + public class ReplicaSpecs + { + [Fact] + public void EvalExpr_with_empty_doc_returns_cursor_with_finalKey_of_DocK() + { + var p0 = Replica.Empty("p"); + var cursor = p0.EvalExpr(new Doc()); + var expected = Cursor.WithFinalKey(new DocK()); + Assert.Equal(expected, cursor); + } + + [Fact] + public void EvalExpr_with_DownField_returns_cursor_with_correct_branchtag_and_key() + { + var p0 = Replica.Empty("p"); + var cursor = p0.EvalExpr(new Doc().DownField("key")); + var expected = new Cursor(List(new MapT(new DocK())), new StrK("key")); + Assert.Equal(expected, cursor); + } + + [Fact] + public void EvalExpr_with_Iter_returns_cursor_with_correct_structure() + { + var p0 = Replica.Empty("p"); + var cursor = p0.EvalExpr(new Doc().Iter()); + var expected = new Cursor(List(new ListT(new DocK())), new HeadK()); + Assert.Equal(expected, cursor); + } + + [Fact] + public void EvalExpr_with_DownField_and_Iter_returns_cursor_with_correct_structure() + { + var p0 = Replica.Empty("p"); + var cursor = p0.EvalExpr(new Doc().DownField("key").Iter()); + var expected = new Cursor(List(new MapT(new DocK()), new ListT(new StrK("key"))), new HeadK()); + Assert.Equal(expected, cursor); + } + + [Fact] + public void EvalExpr_with_Iter_and_Next_returns_cursor_with_correct_structure() + { + var p0 = Replica.Empty("p"); + var cursor = p0.EvalExpr(new Doc().Iter().Next()); + var expected = new Cursor(List(new ListT(new DocK())), new HeadK()); + Assert.Equal(expected, cursor); + } + + [Fact] + public void EvalExpr_with_DownField_and_Iter_and_Next_returns_cursor_with_correct_structure() + { + // property("list.iter.next") = secure { + // val list = doc.downField("list") + // val cmd = (list := `[]`) `;` + // list.iter.insert("item1") `;` + // list.iter.insert("item2") `;` + // list.iter.insert("item3") + + // val p1 = p0.applyCmd(cmd) + // val e1 = list.iter.next + // val e2 = list.iter.next.next + // val e3 = list.iter.next.next.next + // val cur = p1.evalExpr(list.iter) + + // (p1.evalExpr(e1) ?= cur.copy(finalKey = IdK(Id(4, "p")))) && + // (p1.evalExpr(e2) ?= cur.copy(finalKey = IdK(Id(3, "p")))) && + // (p1.evalExpr(e3) ?= cur.copy(finalKey = IdK(Id(2, "p")))) + // } + + var list = new Doc().DownField("list"); + var cmd = list.Assign(new EmptyList()) + .Append(list.Iter().Insert("item1")) + .Append(list.Iter().Insert("item2")) + .Append(list.Iter().Insert("item3")); + + var p0 = Replica.Empty("p"); + var p1 = p0.ApplyCmd(cmd); + var e1 = list.Iter().Next(); + var e2 = list.Iter().Next().Next(); + var e3 = list.Iter().Next().Next().Next(); + var cur = p1.EvalExpr(list.Iter()); + + Assert.Equal(cur.Copy(finalKey: new IdK(4, "p")), p1.EvalExpr(e1)); + Assert.Equal(cur.Copy(finalKey: new IdK(3, "p")), p1.EvalExpr(e2)); + Assert.Equal(cur.Copy(finalKey: new IdK(2, "p")), p1.EvalExpr(e3)); + } + } +} \ No newline at end of file diff --git a/test/Sanity.cs b/test/Sanity.cs new file mode 100644 index 0000000..401a69f --- /dev/null +++ b/test/Sanity.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Numerics; +using LanguageExt; +using static LanguageExt.Prelude; +using Xunit; + +namespace Mingle.Tests +{ + public class Sanity + { + [Theory] + [MemberData(nameof(GetEqualityCheckData))] + public void Check_Equality(object left, object right) + { + Assert.Equal(left, right); + } + + public static IEnumerable GetEqualityCheckData() + { + yield return new [] + { + Replica.Empty(ReplicaId.New("abc")), + Replica.Empty(ReplicaId.New("abc")) + }; + yield return new [] + { + new Id(1, ReplicaId.New("a")), + new Id(1, ReplicaId.New("a")) + }; + yield return new [] + { + new StrK("abc"), + new StrK("abc") + }; + yield return new [] + { + new IdK(new Id(1, ReplicaId.New("a"))), + new IdK(new Id(1, ReplicaId.New("a"))) + }; + yield return new [] + { + Node.EmptyMap, + Node.EmptyMap + }; + yield return new [] + { + new DocK(), + new DocK() + }; + yield return new [] + { + new HeadK(), + new HeadK() + }; + yield return new [] + { + new Var("abc"), + new Var("abc") + }; + yield return new [] + { + new DownField(new Doc(), "key"), + new DownField(new Doc(), "key"), + }; + yield return new [] + { + new AssignM(new Str("abc")), + new AssignM(new Str("abc")) + }; + yield return new [] + { + new DeleteM(), + new DeleteM() + }; + yield return new [] + { + new Doc().DownField("key").Assign("A"), + new Doc().DownField("key").Assign("A") + }; + yield return new [] + { + Replica.Empty(ReplicaId.New("p")).ApplyCmd(new Doc().DownField("key").Assign("A")), + Replica.Empty(ReplicaId.New("p")).ApplyCmd(new Doc().DownField("key").Assign("A")) + }; + yield return new Operation[] + { + new Operation( + new Id(1, ReplicaId.New("a")), + LanguageExt.Set.Empty, + Cursor.Doc(), + new AssignM(new Str("abc")) + ), + new Operation( + new Id(1, ReplicaId.New("a")), + LanguageExt.Set.Empty, + Cursor.Doc(), + new AssignM(new Str("abc")) + ) + }; + yield return new [] + { + new RegNode(LanguageExt.Prelude.Map(( + new Id(1, ReplicaId.New("abc")), + new Str("abc") + ))), + new RegNode(LanguageExt.Prelude.Map(( + new Id(1, ReplicaId.New("abc")), + new Str("adfs") + ))) + }; + } + } +} \ No newline at end of file diff --git a/test/SyntaxSpecs.cs b/test/SyntaxSpecs.cs new file mode 100644 index 0000000..4dd2f39 --- /dev/null +++ b/test/SyntaxSpecs.cs @@ -0,0 +1,34 @@ +using Xunit; + +namespace Mingle.Tests +{ + public class SyntaxSpecs + { + [Fact] + public void Assign() + { + var actual = new Var("list").Assign(new EmptyList()); + var expected = new Assign(new Var("list"), new EmptyList()); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Insert() + { + var actual = new Doc().DownField("key").Iter().Insert(new Null()); + var expected = new Insert(new Iter(new DownField(new Doc(), "key")), new Null()); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Delete() + { + var actual = new Doc().Delete(); + var expected = new Delete(new Doc()); + + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file diff --git a/test/examples/Figure1.cs b/test/examples/Figure1.cs index 377dc04..4da5086 100644 --- a/test/examples/Figure1.cs +++ b/test/examples/Figure1.cs @@ -33,10 +33,10 @@ public void Integration_test() private static void Converged(Replica a, Replica b) { - Assert.True(a.ProcessedOps == b.ProcessedOps);; + Assert.Equal(a.ProcessedOps, b.ProcessedOps); + Assert.Equal(a.Document, b.Document); //a.ProcessedOps.ShouldBeEquivalentTo(b.ProcessedOps); //a.Document.Should().Be(b.Document); - Assert.True(a.Document == b.Document); } private static void Converged(Replica a, Replica b, Replica c) @@ -47,9 +47,9 @@ private static void Converged(Replica a, Replica b, Replica c) private static void Diverged(Replica a, Replica b) { - Assert.True(a.ProcessedOps != b.ProcessedOps); + Assert.NotEqual(a.ProcessedOps, b.ProcessedOps); + Assert.NotEqual(a.Document, b.Document); //a.ProcessedOps.Should().NotBeEquivalentTo(b.ProcessedOps); - Assert.True(a.Document != b.Document); //a.Document.Should().NotBe(b.Document); }