From f599dd8b0845251636a5d0800fb69ebb4d0ed794 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 11:30:33 +0100 Subject: [PATCH 01/22] Update LanguageExt and netcore sdk Also updated README.md with a bit more info --- README.md | 19 ++++++++++++++++++- src/Mingle.csproj | 6 +----- test/Mingle.Tests.csproj | 5 ++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bff005b..fd06055 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,21 @@ | 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 +| `dev` | [![Build Status](https://travis-ci.org/Stephanvs/Mingle.svg?branch=dev)](https://travis-ci.org/Stephanvs/Mingle) | + + +# 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/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/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 From ec0c97b655aeaa85e21f96f4faa9809508f0aee4 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 11:30:46 +0100 Subject: [PATCH 02/22] Make internals visible to test project --- src/Attributes.cs | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/Attributes.cs 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 From 37050922fa41a9292ace17e22f902d51fd135215 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 15:30:50 +0100 Subject: [PATCH 03/22] Properly implemented Record types - Also marked Cursor.ctor as internal and made visible to test project - Implement IComparable on `TypeTag`, `Key`, `Cmd` - Added typed variants for True/False/Null/EmptyList/EmptyMap - Changed `bigint` to `System.Numerics.BigInteger` --- src/Core/Cmd.cs | 80 ++++---- src/Core/Cursor.cs | 29 ++- src/Core/Expr.cs | 30 +-- src/Core/Id.cs | 27 +-- src/Core/Key.cs | 12 +- src/Core/ListRef.cs | 32 ++-- src/Core/Mutation.cs | 24 +-- src/Core/Node.cs | 174 +++++++++++++----- src/Core/Operation.cs | 24 +-- src/Core/Replica.cs | 49 +++-- src/Core/ReplicaId.cs | 2 +- src/Core/Syntax/CmdOps.cs | 7 +- src/Core/Syntax/ExprOps.cs | 24 +++ src/Core/TypeTag.cs | 62 ++++--- src/Core/Val.cs | 33 +++- test/Given_a_Replica/When_empty.cs | 3 +- .../When_incrementing_counter.cs | 3 +- 17 files changed, 342 insertions(+), 273 deletions(-) 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..884ab48 100644 --- a/src/Core/Cursor.cs +++ b/src/Core/Cursor.cs @@ -3,18 +3,17 @@ namespace Mingle { - public /* immutable */ class Cursor + public /* immutable */ 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); @@ -38,30 +37,24 @@ public interface IView public 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 { - 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..b274ec5 100644 --- a/src/Core/Id.cs +++ b/src/Core/Id.cs @@ -1,27 +1,20 @@ -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, 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..e82f18f 100644 --- a/src/Core/Key.cs +++ b/src/Core/Key.cs @@ -19,15 +19,13 @@ public class HeadK : Record, Key public sealed class IdK : Record, Key { - private readonly Id _id; + public readonly Id Id; public IdK(Id id) { - _id = id; + Id = id; } - public Id Id => _id; - public int CompareTo(object obj) { return obj is IdK id @@ -38,15 +36,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..d142b67 100644 --- a/src/Core/ListRef.cs +++ b/src/Core/ListRef.cs @@ -2,19 +2,6 @@ namespace Mingle { - public abstract class ListRef - { - public static ListRef FromKey(Key key) - { - switch (key) - { - case IdK k: return new IdR(k.Id); - case HeadK _: return new HeadR(); - default: return new TailR(); - } - } - } - public abstract class KeyRef : ListRef { public Key ToKey() @@ -26,12 +13,25 @@ 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 abstract class ListRef + { + public static ListRef FromKey(Key key) + { + switch (key) + { + case IdK k: return new IdR(k.Id); + case HeadK _: return new HeadR(); + default: return new TailR(); + } } } - public class IdR : KeyRef + public sealed class IdR : KeyRef { public IdR(Id id) { 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..21049bd 100644 --- a/src/Core/Node.cs +++ b/src/Core/Node.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using LanguageExt; using static LanguageExt.Prelude; @@ -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); @@ -74,6 +75,33 @@ 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 ln: ListNode if (ln.orderArchive get op.id.c).isEmpty => + // ln.copy(orderArchive = ln.orderArchive + (op.id.c -> ln.order)) + 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,7 +113,7 @@ 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); @@ -113,7 +141,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 +222,43 @@ 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. + return ApplyAtLeaf(op.Copy(cursor: Cursor.WithFinalKey(new IdK(nextId.Id))), replica); + } + // 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: { @@ -226,23 +290,32 @@ private Node AddId(TypeTag tag, Id id, Mutation mutation) { switch (mutation) { - case DeleteM del: return this; - default: { + case DeleteM del: + return this; + 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 +409,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 +426,33 @@ 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.Add(src, dst)); + } + default: return this; + } + } } 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); @@ -412,27 +502,23 @@ private MapNode Copy( public class ListNode : BranchNode { - private readonly Map _order; + public readonly Map Order; [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(); @@ -443,42 +529,30 @@ public override BranchNode WithPresSets(Map> presSets) throw new NotImplementedException(); } - // /** 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; - // } - - // return base.Equals(obj); - // } - - // public override int GetHashCode() - // { - // throw new NotImplementedException(); - // } + internal ListNode Copy( + Map? order = null, + Map>? orderArchive = null) + => new ListNode( + children: Children, + 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); } } \ 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..3fb40c3 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: @@ -216,7 +209,7 @@ public static Replica ApplyCmds(Replica replica, Lst cmds) public static Replica Empty(ReplicaId replicaId) => new Replica( replicaId, - opsCounter: bigint.Zero, + opsCounter: BigInteger.Zero, document: Node.EmptyMap, variables: Map(), processedOps: Set(), @@ -225,7 +218,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/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; From ddb46fb3ae1e29ba3da37464cb6573bc69820d42 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 15:31:02 +0100 Subject: [PATCH 04/22] Fix formatting --- test/Given_a_Doc/When_DownField.cs | 1 - 1 file changed, 1 deletion(-) 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 From 7418d2a8b83915f5df0f7c8c14884bfe8b00084a Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 16:00:54 +0100 Subject: [PATCH 05/22] Added simple sanity checks --- test/Sanity.cs | 113 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/Sanity.cs 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 From fec171219493e62b7b049e1cbb06b30105da8bd2 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 16:01:31 +0100 Subject: [PATCH 06/22] Added Replica tests --- test/ReplicaSpecs.cs | 89 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 test/ReplicaSpecs.cs diff --git a/test/ReplicaSpecs.cs b/test/ReplicaSpecs.cs new file mode 100644 index 0000000..8a13dbe --- /dev/null +++ b/test/ReplicaSpecs.cs @@ -0,0 +1,89 @@ +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(ReplicaId.New("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(ReplicaId.New("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(ReplicaId.New("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(ReplicaId.New("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(ReplicaId.New("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(ReplicaId.New("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()); + } + } +} \ No newline at end of file From c4a1669177cdedf5d6748b386b8aedee63fd607c Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 16:11:53 +0100 Subject: [PATCH 07/22] Use Assert.Equal instead of Assert.True --- test/examples/Figure1.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/examples/Figure1.cs b/test/examples/Figure1.cs index 377dc04..6b53160 100644 --- a/test/examples/Figure1.cs +++ b/test/examples/Figure1.cs @@ -2,8 +2,8 @@ using Xunit; using FluentAssertions; using LanguageExt; -using static LanguageExt.Prelude; using Newtonsoft.Json; +using static LanguageExt.Prelude; namespace Mingle { @@ -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); //a.ProcessedOps.ShouldBeEquivalentTo(b.ProcessedOps); //a.Document.Should().Be(b.Document); - Assert.True(a.Document == b.Document); + Assert.Equal(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); //a.ProcessedOps.Should().NotBeEquivalentTo(b.ProcessedOps); - Assert.True(a.Document != b.Document); + Assert.NotEqual(a.Document, b.Document); //a.Document.Should().NotBe(b.Document); } From a1ab41d567850a12463a0de410d9b7960cffe56f Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 16:15:45 +0100 Subject: [PATCH 08/22] Minor reformatting --- src/Core/Node.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/Node.cs b/src/Core/Node.cs index 21049bd..3568ae8 100644 --- a/src/Core/Node.cs +++ b/src/Core/Node.cs @@ -290,8 +290,7 @@ private Node AddId(TypeTag tag, Id id, Mutation mutation) { switch (mutation) { - case DeleteM del: - return this; + case DeleteM del: return this; default: { var pres = GetPres(tag.Key); @@ -504,6 +503,9 @@ public class ListNode : BranchNode { 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] public readonly Map> OrderArchive; From c5d187dd9a1fc1702e9560188a7f03d94c340768 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Mon, 29 Oct 2018 16:16:01 +0100 Subject: [PATCH 09/22] Minor formatting --- test/examples/Figure1.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/examples/Figure1.cs b/test/examples/Figure1.cs index 6b53160..4da5086 100644 --- a/test/examples/Figure1.cs +++ b/test/examples/Figure1.cs @@ -2,8 +2,8 @@ using Xunit; using FluentAssertions; using LanguageExt; -using Newtonsoft.Json; using static LanguageExt.Prelude; +using Newtonsoft.Json; namespace Mingle { @@ -34,9 +34,9 @@ public void Integration_test() private static void Converged(Replica a, Replica b) { Assert.Equal(a.ProcessedOps, b.ProcessedOps); + Assert.Equal(a.Document, b.Document); //a.ProcessedOps.ShouldBeEquivalentTo(b.ProcessedOps); //a.Document.Should().Be(b.Document); - Assert.Equal(a.Document, b.Document); } private static void Converged(Replica a, Replica b, Replica c) @@ -48,8 +48,8 @@ private static void Converged(Replica a, Replica b, Replica c) private static void Diverged(Replica a, Replica b) { Assert.NotEqual(a.ProcessedOps, b.ProcessedOps); - //a.ProcessedOps.Should().NotBeEquivalentTo(b.ProcessedOps); Assert.NotEqual(a.Document, b.Document); + //a.ProcessedOps.Should().NotBeEquivalentTo(b.ProcessedOps); //a.Document.Should().NotBe(b.Document); } From babad34fdcdf9cbbc2402bf15e8fa4420ef69195 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Tue, 30 Oct 2018 11:12:22 +0100 Subject: [PATCH 10/22] Remove travis ci --- .travis.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .travis.yml 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 From 43db22929a3c4f564b32380552a0d2655b9a3226 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Wed, 31 Oct 2018 08:38:36 +0100 Subject: [PATCH 11/22] Removed unused comment --- src/Core/Node.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Node.cs b/src/Core/Node.cs index 3568ae8..6d6f789 100644 --- a/src/Core/Node.cs +++ b/src/Core/Node.cs @@ -88,8 +88,6 @@ private Node SaveOrder(Operation operation) { switch (this) { - // case ln: ListNode if (ln.orderArchive get op.id.c).isEmpty => - // ln.copy(orderArchive = ln.orderArchive + (op.id.c -> ln.order)) case ListNode ln: { if (ln.OrderArchive.Exists((k, v) => k == operation.Id.OpsCounter && v.IsEmpty)) From b6869e8a17398f800f7502a7e3ff1a2db034242d Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Wed, 31 Oct 2018 08:42:17 +0100 Subject: [PATCH 12/22] Update azure pipeline to properly publish test results Includes code coverage results --- azure-pipelines.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cc30f67..3e7edda 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,5 +12,9 @@ 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 + inputs: + command: test + projects: '**/*Tests/*.csproj' + arguments: '--configuration $(buildConfiguration) --collect "Code coverage"' +- task: PublishBuildArtifacts@1 \ No newline at end of file From b312d2bc56af0d912de6d850ee455095505c986b Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Wed, 31 Oct 2018 08:58:44 +0100 Subject: [PATCH 13/22] Scan for tests in correct location --- azure-pipelines.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3e7edda..122f832 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,9 +12,16 @@ variables: steps: - script: dotnet build --configuration $(buildConfiguration) displayName: 'dotnet build $(buildConfiguration)' + - task: DotNetCoreCLI@2 + displayName: 'run unit tests' inputs: command: test - projects: '**/*Tests/*.csproj' + projects: 'test/**/*.csproj' arguments: '--configuration $(buildConfiguration) --collect "Code coverage"' -- task: PublishBuildArtifacts@1 \ No newline at end of file + +- script: dotnet publish --output $(Build.ArtifactStagingDirectory) + displayName: 'dotnet publish' + +- task: PublishBuildArtifacts@1 + displayName: 'publish build artifacts' \ No newline at end of file From e4fa76e6b48f0f545b247925e75335fb4d2cd1c1 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Wed, 31 Oct 2018 09:24:11 +0100 Subject: [PATCH 14/22] Update build status badges --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fd06055..40cfde7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ | 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) | - +| `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? From e29b6328d78fdf53677d75f222c56e4c621c4bb5 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Fri, 2 Nov 2018 22:32:47 +0100 Subject: [PATCH 15/22] Added additional assertions to the spec --- test/ReplicaSpecs.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/ReplicaSpecs.cs b/test/ReplicaSpecs.cs index 8a13dbe..6f0826d 100644 --- a/test/ReplicaSpecs.cs +++ b/test/ReplicaSpecs.cs @@ -84,6 +84,10 @@ public void EvalExpr_with_DownField_and_Iter_and_Next_returns_cursor_with_correc var e2 = list.Iter().Next().Next(); var e3 = list.Iter().Next().Next().Next(); var cur = p1.EvalExpr(list.Iter()); + + Assert.Equal(p1.EvalExpr(e1), cur.Copy(finalKey: new IdK(new Id(4, ReplicaId.New("p"))))); + Assert.Equal(p1.EvalExpr(e2), cur.Copy(finalKey: new IdK(new Id(3, ReplicaId.New("p"))))); + Assert.Equal(p1.EvalExpr(e3), cur.Copy(finalKey: new IdK(new Id(2, ReplicaId.New("p"))))); } } } \ No newline at end of file From ebc47b4451c64b46b4974421853a006ee8c02957 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Fri, 2 Nov 2018 22:33:35 +0100 Subject: [PATCH 16/22] Handle branch cases within the Cursor.View() --- src/Core/Cursor.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Core/Cursor.cs b/src/Core/Cursor.cs index 884ab48..cf428a6 100644 --- a/src/Core/Cursor.cs +++ b/src/Core/Cursor.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using LanguageExt; +using static LanguageExt.Prelude; namespace Mingle { @@ -21,9 +23,9 @@ public Cursor Append(Func tag, Key newFinalKey) } public Cursor.IView View() - { - return new Leaf(FinalKey); - } + => match(Keys, + () => new Leaf(this.FinalKey), + (k1, kn) => new Branch(k1, new Cursor(kn.Freeze(), FinalKey))); public static Cursor Doc() => WithFinalKey(new DocK()); @@ -31,6 +33,9 @@ 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 { } From f0dc1342077118c507bb7e3f85c296874f13b22b Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Wed, 7 Nov 2018 21:29:16 +0100 Subject: [PATCH 17/22] Added SyntaxSpecs --- test/SyntaxSpecs.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/SyntaxSpecs.cs 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 From e37bf0d531c9cd6054028c2cb977b129a00fec1c Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Fri, 9 Nov 2018 20:29:17 +0100 Subject: [PATCH 18/22] Introduced ctor overloads and helper methods for simpler api surface --- src/Core/Cursor.cs | 7 ++----- src/Core/Id.cs | 3 +++ src/Core/Key.cs | 4 ++++ src/Core/Replica.cs | 3 +++ test/ReplicaSpecs.cs | 18 +++++++++--------- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Core/Cursor.cs b/src/Core/Cursor.cs index cf428a6..7c866e7 100644 --- a/src/Core/Cursor.cs +++ b/src/Core/Cursor.cs @@ -5,7 +5,7 @@ namespace Mingle { - public /* immutable */ class Cursor : Record + public /* immutable */ sealed class Cursor : Record { public readonly Lst Keys; public readonly Key FinalKey; @@ -17,10 +17,7 @@ internal Cursor(Lst keys, Key finalKey) } 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() => match(Keys, diff --git a/src/Core/Id.cs b/src/Core/Id.cs index b274ec5..dc957f1 100644 --- a/src/Core/Id.cs +++ b/src/Core/Id.cs @@ -11,6 +11,9 @@ namespace Mingle public readonly BigInteger OpsCounter; public readonly ReplicaId ReplicaId; + public Id(BigInteger opsCounter, string replicaId) + : this(opsCounter, new ReplicaId(replicaId)) {} + public Id(BigInteger opsCounter, ReplicaId replicaId) { OpsCounter = opsCounter; diff --git a/src/Core/Key.cs b/src/Core/Key.cs index e82f18f..ee9a1a2 100644 --- a/src/Core/Key.cs +++ b/src/Core/Key.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using LanguageExt; namespace Mingle @@ -21,6 +22,9 @@ public sealed class IdK : Record, Key { public readonly Id Id; + public IdK(BigInteger opsCounter, string replicaId) + : this(new Id(opsCounter, replicaId)) {} + public IdK(Id id) { Id = id; diff --git a/src/Core/Replica.cs b/src/Core/Replica.cs index 3fb40c3..b60001d 100644 --- a/src/Core/Replica.cs +++ b/src/Core/Replica.cs @@ -206,6 +206,9 @@ 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, diff --git a/test/ReplicaSpecs.cs b/test/ReplicaSpecs.cs index 6f0826d..ce613f6 100644 --- a/test/ReplicaSpecs.cs +++ b/test/ReplicaSpecs.cs @@ -9,7 +9,7 @@ public class ReplicaSpecs [Fact] public void EvalExpr_with_empty_doc_returns_cursor_with_finalKey_of_DocK() { - var p0 = Replica.Empty(ReplicaId.New("p")); + var p0 = Replica.Empty("p"); var cursor = p0.EvalExpr(new Doc()); var expected = Cursor.WithFinalKey(new DocK()); Assert.Equal(expected, cursor); @@ -18,7 +18,7 @@ public void EvalExpr_with_empty_doc_returns_cursor_with_finalKey_of_DocK() [Fact] public void EvalExpr_with_DownField_returns_cursor_with_correct_branchtag_and_key() { - var p0 = Replica.Empty(ReplicaId.New("p")); + 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); @@ -27,7 +27,7 @@ public void EvalExpr_with_DownField_returns_cursor_with_correct_branchtag_and_ke [Fact] public void EvalExpr_with_Iter_returns_cursor_with_correct_structure() { - var p0 = Replica.Empty(ReplicaId.New("p")); + 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); @@ -36,7 +36,7 @@ public void EvalExpr_with_Iter_returns_cursor_with_correct_structure() [Fact] public void EvalExpr_with_DownField_and_Iter_returns_cursor_with_correct_structure() { - var p0 = Replica.Empty(ReplicaId.New("p")); + 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); @@ -45,7 +45,7 @@ public void EvalExpr_with_DownField_and_Iter_returns_cursor_with_correct_structu [Fact] public void EvalExpr_with_Iter_and_Next_returns_cursor_with_correct_structure() { - var p0 = Replica.Empty(ReplicaId.New("p")); + 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); @@ -78,16 +78,16 @@ public void EvalExpr_with_DownField_and_Iter_and_Next_returns_cursor_with_correc .Append(list.Iter().Insert("item2")) .Append(list.Iter().Insert("item3")); - var p0 = Replica.Empty(ReplicaId.New("p")); + 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(p1.EvalExpr(e1), cur.Copy(finalKey: new IdK(new Id(4, ReplicaId.New("p"))))); - Assert.Equal(p1.EvalExpr(e2), cur.Copy(finalKey: new IdK(new Id(3, ReplicaId.New("p"))))); - Assert.Equal(p1.EvalExpr(e3), cur.Copy(finalKey: new IdK(new Id(2, ReplicaId.New("p"))))); + 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 From 19463992def9cb8ee8ca3edc81a91e2b80ad81b3 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Fri, 9 Nov 2018 21:15:34 +0100 Subject: [PATCH 19/22] Proper IComparable for Node, Replica and ListRef --- src/Core/ListRef.cs | 47 ++++++++++++++++++--------- src/Core/Node.cs | 77 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/src/Core/ListRef.cs b/src/Core/ListRef.cs index d142b67..a6cd3d6 100644 --- a/src/Core/ListRef.cs +++ b/src/Core/ListRef.cs @@ -1,7 +1,23 @@ using System; +using LanguageExt; namespace Mingle { + public abstract class ListRef : Record, IComparable + { + public static ListRef FromKey(Key key) + { + switch (key) + { + case IdK k: return new IdR(k.Id); + case HeadK _: return new HeadR(); + default: return new TailR(); + } + } + + public abstract int CompareTo(object other); + } + public abstract class KeyRef : ListRef { public Key ToKey() @@ -16,32 +32,33 @@ public Key ToKey() $"Cannot convert {this.GetType().Name} using method '{nameof(ToKey)}' to {nameof(Key)}. " + $"Are you illegaly subclassing {nameof(KeyRef)}?"); } - } - public abstract class ListRef - { - public static ListRef FromKey(Key key) - { - switch (key) - { - case IdK k: return new IdR(k.Id); - case HeadK _: return new HeadR(); - default: return new TailR(); - } - } + public override int CompareTo(object other) + => RecordType.Compare(this, other as 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/Node.cs b/src/Core/Node.cs index 6d6f789..e5bf7f5 100644 --- a/src/Core/Node.cs +++ b/src/Core/Node.cs @@ -7,7 +7,7 @@ namespace Mingle { - public abstract class Node : Record + public abstract class Node : Record, IComparable { public static Node EmptyMap => new MapNode( @@ -40,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); } } @@ -51,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: @@ -115,7 +116,17 @@ 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); @@ -238,7 +249,9 @@ private Node ApplyAtLeaf(Operation op, Replica replica) // 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. - return ApplyAtLeaf(op.Copy(cursor: Cursor.WithFinalKey(new IdK(nextId.Id))), replica); + 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 @@ -430,11 +443,13 @@ private Node SetNextRef(ListRef src, ListRef dst) { case ListNode ln: { - return ln.Copy(order: ln.Order.Add(src, dst)); + return ln.Copy(order: ln.Order.AddOrUpdate(src, dst)); } default: return this; } } + + public abstract int CompareTo(object obj); } public abstract class BranchNode : Node @@ -470,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) @@ -520,21 +547,28 @@ public ListNode( } public override BranchNode WithChildren(Map children) - { - throw new NotImplementedException(); - } + => Copy(children: children); public override BranchNode WithPresSets(Map> presSets) - { - throw new NotImplementedException(); - } + => Copy(presSets: presSets); + + public override bool Equals(object other) + => RecordType.EqualityTyped(this, other as ListNode); + + public override int GetHashCode() + => RecordType.Hash(this); + + 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, - presSets: PresSets, + children: children ?? Children, + presSets: presSets ?? PresSets, order: order ?? Order, orderArchive: orderArchive ?? OrderArchive ); @@ -554,5 +588,14 @@ public Lst Values() public RegNode Copy(Map? regValues = null) => 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 From 57b2d981c81bf83cac6f1ae434c30808f5e9789c Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Fri, 9 Nov 2018 21:18:35 +0100 Subject: [PATCH 20/22] Add to end instead of at beginning of pipeline --- src/Core/Replica.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Replica.cs b/src/Core/Replica.cs index b60001d..716a935 100644 --- a/src/Core/Replica.cs +++ b/src/Core/Replica.cs @@ -132,7 +132,7 @@ Cursor f(Cursor c) Cursor f(Cursor c) => c.Append(k => new ListT(k), new HeadK()); - return Go(it.Expr, fs.Insert(0, f)); + return Go(it.Expr, fs.Add(f)); } case Next next: From e87b0bd779fb63896f159c3461cded98e10fd7bd Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Fri, 9 Nov 2018 23:12:07 +0100 Subject: [PATCH 21/22] Resolve back to inserting in front of fs --- src/Core/Replica.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Replica.cs b/src/Core/Replica.cs index 716a935..b60001d 100644 --- a/src/Core/Replica.cs +++ b/src/Core/Replica.cs @@ -132,7 +132,7 @@ Cursor f(Cursor c) Cursor f(Cursor c) => c.Append(k => new ListT(k), new HeadK()); - return Go(it.Expr, fs.Add(f)); + return Go(it.Expr, fs.Insert(0, f)); } case Next next: From a2e3500e6bcf5e58301a22f48a33de551a572ff8 Mon Sep 17 00:00:00 2001 From: Stephan van Stekelenburg Date: Sat, 10 Nov 2018 14:46:57 +0100 Subject: [PATCH 22/22] Mark more classes as sealed, and minor refactoring --- src/Core/Cursor.cs | 6 +++--- src/Core/Key.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Core/Cursor.cs b/src/Core/Cursor.cs index 7c866e7..1f25bcf 100644 --- a/src/Core/Cursor.cs +++ b/src/Core/Cursor.cs @@ -21,7 +21,7 @@ public Cursor Append(Func tag, Key newFinalKey) public Cursor.IView View() => match(Keys, - () => new Leaf(this.FinalKey), + () => new Leaf(FinalKey), (k1, kn) => new Branch(k1, new Cursor(kn.Freeze(), FinalKey))); public static Cursor Doc() @@ -37,7 +37,7 @@ public interface IView { } - public class Leaf : Record, IView + public sealed class Leaf : Record, IView { public readonly Key FinalKey; @@ -47,7 +47,7 @@ public Leaf(Key finalKey) } } - public class Branch : Record, IView + public sealed class Branch : Record, IView { public readonly BranchTag Head; public readonly Cursor Tail; diff --git a/src/Core/Key.cs b/src/Core/Key.cs index ee9a1a2..413a328 100644 --- a/src/Core/Key.cs +++ b/src/Core/Key.cs @@ -8,12 +8,12 @@ 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; }