diff --git a/.gitignore b/.gitignore index f9d5d37..7bf7e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /tests_results /main.exe /main.exe.mdb -/*Generated.cs -/*Generator.exe \ No newline at end of file +*Generated.cs +*GeneratedF.cs +*Generator.exe diff --git a/F.sed b/F.sed new file mode 100644 index 0000000..13bffc0 --- /dev/null +++ b/F.sed @@ -0,0 +1,38 @@ +#!/usr/bin/env sed -nf + +/^[^ ].*/p + +/\[F\]/,/^ *)/{ + s/\( *\)private partial class \(.*\) {/\1\2/; + t classHeader; + b notClassHeader; + :classHeader + h; + s/\( *\)\(.*\)/\1private partial class \2 : IEqF {\n\1 private \2() {}\n\1 public static readonly \2\3 Eq = new \2\3();\n\1 public static bool operator ==(\2\3 a, \2\3 b)\n\1 => Equality.Operator(a, b);\n\1 public static bool operator !=(\2\3 a, \2\3 b)\n\1 => !(a == b);\n\1 public override bool Equals(object other)\n\1 => Equality.Untyped<\2\3>(\n\1 this,\n\1 other,\n\1 x => x as \2\3,\n\1 x => x.hashCode);\n\1 public bool Equals(IEqF<\n\5\n\1 \4\n\1 > other)\n\1 => Equality.Equatable>(this, other);\n\1 private int hashCode = HashCode.Combine("\2\3");\n\1 public override int GetHashCode() => hashCode;\n\1 public override string ToString() => "Equatable function \2\3()";\n\1}\n/; + + s/\( *\)\([^\n<]*\)\([^\n]*\)\n\([^\n]*\)\n\(.*\)$/\1 \4\n >, IEquatable<\2\3> {\n\1 private \2() {}\n\1 public static readonly \2\3 Eq = new \2\3();\n\1 public static bool operator ==(\2\3 a, \2\3 b)\n\1 => Equality.Operator(a, b);\n\1 public static bool operator !=(\2\3 a, \2\3 b)\n\1 => !(a == b);\n\1 public override bool Equals(object other)\n\1 => Equality.Untyped<\2\3>(\n\1 this,\n\1 other,\n\1 x => x as \2\3,\n\1 x => x.hashCode);\n\1 public bool Equals(\2\3 other)\n\1 => Equality.Equatable<\2\3>(this, other);\n\1 private int hashCode = HashCode.Combine("\2\3");\n\1 public override int GetHashCode() => hashCode;\n\1 public override string ToString() => "Equatable function \2\3()";\n\1}\n/; + + p; + # Clear hold space + s/.*//; + h; + :next + } +} \ No newline at end of file diff --git a/Lexer.cs b/Lexer.cs index 7e0b054..9f04d78 100644 --- a/Lexer.cs +++ b/Lexer.cs @@ -1,5 +1,4 @@ using System; -using System.Text; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -127,7 +126,7 @@ public static partial class Lexer { } } - private static IEnumerable Transition(ref S state, ref string lexeme, GraphemeCluster c, Rule rule) { + private static ValueTuple> Transition(S state, string lexeme, GraphemeCluster c, Rule rule) { List result = new List(); if (rule.throughState != state) { result.Add(new Lexeme(state, lexeme)); @@ -140,13 +139,19 @@ public static partial class Lexer { state = rule.newState; lexeme = ""; } - return result; + return (state, lexeme, result.GetImmutableEnumerator()); } - public static ParserErrorException ParserError(StringBuilder context, IEnumerator stream, S state, List possibleNext, GraphemeCluster gc) { - var rest = - stream - .SingleUseEnumerable() + public static ParserErrorException ParserError(IImmutableEnumerator context, IImmutableEnumerator rest, S state, List possibleNext, GraphemeCluster gc) { + var strContext = + context + .ToIEnumerable() + .TakeUntil(c => c.Equals(rest)) + .Select(c => c.str) + .JoinWith(""); + + var strRest = + rest .TakeUntil(c => c.str.StartsWith("\n")) .Select(c => c.str) .JoinWith(""); @@ -158,39 +163,72 @@ public static partial class Lexer { .Match(Some: x => x.UnicodeCategory(0).ToString(), None: "None (empty string)"); return new ParserErrorException( - $"Unexpected {actual} (Unicode category {cat}) while the lexer was in state {state}: expected one of {expected}{Environment.NewLine}{context} <--HERE {rest}" + $"Unexpected {actual} (Unicode category {cat}) while the lexer was in state {state}: expected one of {expected}{Environment.NewLine}{strContext} <--HERE {strRest}" ); } // fake Unicode category private const UnicodeCategory EndOfFile = (UnicodeCategory)(-1); - public static IEnumerable> Lex1(string source) { - var context = new StringBuilder(); - var lexeme = ""; - var state = S.Space; - var e = source.TextElements().GetEnumerator(); - while (e.MoveNext()) { - var c = e.Current; - context.Append(c.str); + public static U Foo(T x, Func f) => f(x); + + [F] + private partial class Flub { + public ValueTuple>, IImmutableEnumerator> F( + ValueTuple> t, + ValueTuple> cur + ) + { + var (lexeme, state, context) = t; + var (c, current) = cur; var possibleNext = Rules.WithEpsilonTransitions[state]; - yield return + + return possibleNext .First(r => r.test(c)) - .IfSome(rule => Transition(ref state, ref lexeme, c, rule)) - .ElseThrow(() => ParserError(context, e, state, possibleNext, c)); + .IfSome(rule => { + var r = Transition(state, lexeme, c, rule); + var newState = r.Item1; + var newLexeme = r.Item2; + var tokens = r.Item3; + return ((newLexeme, newState, context), tokens); + }) + .ElseThrow(() => ParserError(context, current, state, possibleNext, c)); } } - public static IEnumerable Lex(string source) { - var first = true; - foreach (var x in Lex1(source).SelectMany(x => x)) { - if (first && "".Equals(x.lexeme)) { - // skip the initial empty whitespace - } else { - first = false; - yield return x; - } - } + public static IImmutableEnumerator> Lex2(IImmutableEnumerator ie) { + var lexeme = ""; + var state = S.Space; + // In a REPL we could reset the context at the end of each statement. + // We could also reset the context to the containing line or function when processing files. + var context = ie; + + return ie.SelectAggregate((lexeme, state, context), Flub.Eq); } + + public static IImmutableEnumerator> + Lex1(string source) + => Lex2(source.TextElements().GetImmutableEnumerator()); + + [F] + private partial class SkipInitialEmptyWhitespace { + public IImmutableEnumerator F( + IImmutableEnumerator lx + ) + => lx.FirstAndRest().Match>, IImmutableEnumerator>( + Some: hdtl => + // skip the initial empty whitespace + string.Equals( + "", + hdtl.Item1.lexeme) + ? hdtl.Item2 + : hdtl.Item1.ImSingleton().Concat(hdtl.Item2), + None: Empty()); + } + + public static IImmutableEnumerator Lex(string source) + => Lex1(source) + .Flatten() + .Lazy(SkipInitialEmptyWhitespace.Eq); } \ No newline at end of file diff --git a/Makefile b/Makefile index 621b413..da02685 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,13 @@ CS := $(shell find . -not \( -path ./.git \) -not \( -name '*Generator.cs' \) -n META := $(shell find ./T4/ -name '*.cs') GENERATORS := $(shell find . -not \( -path ./.git \) -not \( -path './T4/*' \) -name '*Generator.cs') GENERATED := $(patsubst %Generator.cs,%Generated.cs,$(GENERATORS)) +GENERATEDF := $(patsubst %.cs,%GeneratedF.cs,$(shell git grep -l '\[F\]' | grep '\.cs$$')) .PHONY: run run: main.exe Makefile MONO_PATH=/usr/lib/mono/4.5/:/usr/lib/mono/4.5/Facades/ mono $< -main.exe: $(CS) $(GENERATED) Makefile +main.exe: $(CS) $(GENERATED) $(GENERATEDF) Makefile @echo 'Compiling…' @mcs -debug+ -out:$@ \ /reference:/usr/lib/mono/4.5/System.Collections.Immutable.dll \ @@ -17,7 +18,7 @@ main.exe: $(CS) $(GENERATED) Makefile %Generated.cs: .%Generator.exe Makefile @echo 'Running code generator…' @MONO_PATH=/usr/lib/mono/4.5/:/usr/lib/mono/4.5/Facades/ \ - mono $(filter-out Makefile, $<) + mono $< .%Generator.exe: %Generator.cs $(META) Makefile @echo 'Compiling code generator…' @@ -26,4 +27,7 @@ main.exe: $(CS) $(GENERATED) Makefile /reference:/usr/lib/mono/4.5/Facades/netstandard.dll \ $(filter-out Makefile, $^) +%GeneratedF.cs: %.cs F.sed Makefile + @echo 'Running code generator…' + @sed -n -f F.sed $< > $@ diff --git a/Parser.cs b/Parser.cs index e9db980..e6a9ef9 100644 --- a/Parser.cs +++ b/Parser.cs @@ -19,49 +19,23 @@ public static partial class Parser { throw new NotImplementedException(); } - public static Option BindFold(this IEnumerable e, A init, Func> f) { - var acc = init; - foreach (var x in e) { - var @new = f(acc, x); - if (@new.IsNone) { - return Option.None(); - } else { - acc = @new.ElseThrow(() => new Exception("impossible")); - } - } - return acc.Some(); - } - - public static A WhileSome(A init, Func> f) { - var lastGood = init; - while (true) { - var @new = f(lastGood); - if (@new.IsNone) { - return lastGood; - } else { - lastGood = @new.ElseThrow(() => new Exception("impossible")); - } - } - } - - public static Option> Parse3( + public static Option> Parse3( Func, - Option>> + IImmutableEnumerator, + Option>> Parse3, Grammar2 grammar, - ImmutableEnumerator tokens + IImmutableEnumerator tokens ) => tokens - .MoveNext() - .Match>, Option>>( + .FirstAndRest() + .Match( None: () => throw new Exception("EOF, what to do?"), Some: headRest => { - throw new Exception("NIY"); var first = headRest.Item1; var rest = headRest.Item2; - grammar.Match>>( + return grammar.Match( RepeatOnePlus: g => Parse3(g, rest) .IfSome(rest1 => @@ -78,7 +52,7 @@ public static partial class Parser { Terminal: t => first.state.Equals(t) ? rest.Some() - : None>() + : None>() ); // TODO: at the top-level, check that the lexemes // are empty if the parser won't accept anything else. diff --git a/Utils/Enumerable.cs b/Utils/Enumerable.cs index 6dfbf07..bbb74ee 100644 --- a/Utils/Enumerable.cs +++ b/Utils/Enumerable.cs @@ -235,4 +235,32 @@ public static class Collection { } } } + + public static Option BindFold(this IEnumerable e, A init, Func> f) { + var acc = init; + foreach (var x in e) { + var @new = f(acc, x); + if (@new.IsNone) { + return Option.None(); + } else { + acc = @new.ElseThrow(() => new Exception("impossible")); + } + } + return acc.Some(); + } + + public static A WhileSome(this A init, Func> f) { + var lastGood = init; + while (true) { + var @new = f(lastGood); + if (@new.IsNone) { + return lastGood; + } else { + lastGood = @new.ElseThrow(() => new Exception("impossible")); + } + } + } + + public static Option> WhileSome(this Option> init, Func>> f) + => init.IfSome(ab1 => WhileSome(ab1, ab => f(ab.Item1, ab.Item2))); } \ No newline at end of file diff --git a/Utils/Func.cs b/Utils/Func.cs index 6fc41a5..a8a265a 100644 --- a/Utils/Func.cs +++ b/Utils/Func.cs @@ -70,4 +70,50 @@ public static class Func { public static C YMemoized(this Func, A, B, C> f, A a, B b) where A : IEquatable where B : IEquatable => f.YMemoize()(a, b); -} \ No newline at end of file +} + +// IEquatableFunction +// Possible with if we remove the IEquatable constraint +public interface IEqF {// : IEquatable> { + U F(T x); +} + +public interface IEqF {// : IEquatable> { + U F(T1 x, T2 y); +} + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public class F : System.Attribute {} + +public class PartialEqF : IEqF, IEquatable> { + private readonly IEqF f; + private readonly T1 arg1; + public PartialEqF(IEqF f, T1 arg1) { + this.f = f; + this.arg1 = arg1; + hashCode = Equality.HashCode("PartialEqF", f, arg1); + } + public U F(T2 arg2) => f.F(arg1, arg2); + public static bool operator ==(PartialEqF a, PartialEqF b) + => Equality.Operator(a, b); + public static bool operator !=(PartialEqF a, PartialEqF b) + => !(a == b); + public override bool Equals(object other) + => Equality.Untyped>( + this, + other, + x => x as PartialEqF, + x => x.hashCode, + x => x.f, + x => x.arg1); + public bool Equals(PartialEqF other) + => Equality.Equatable>(this, other); + private int hashCode; + public override int GetHashCode() => hashCode; + public override string ToString() => "Equatable function PartialEqF()"; +} + +public static class EqFExtensionMethods { + public static IEqF ImPartial(this IEqF f, T1 arg1) + => new PartialEqF(f, arg1); +} diff --git a/Utils/Global.cs b/Utils/Global.cs index 823e118..38fded5 100644 --- a/Utils/Global.cs +++ b/Utils/Global.cs @@ -22,4 +22,13 @@ public static class Global { => xs.ToImmutableHashSet(); public static T To(this T x) => x; + + public static A WhileSome(A init, Func> f) + => Collection.WhileSome(init, f); + + public static Option> WhileSome(Option> init, Func>> f) + => Collection.WhileSome(init, f); + + public static IImmutableEnumerator Empty() + => ImmutableEnumeratorExtensionMethods.Empty(); } \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/ExtensionMethods.cs b/Utils/Immutable/Enumerator/ExtensionMethods.cs new file mode 100644 index 0000000..3c28d58 --- /dev/null +++ b/Utils/Immutable/Enumerator/ExtensionMethods.cs @@ -0,0 +1,220 @@ +// Code quality of this file: low. +// We need an annotation on lambdas to lift them to equatable singletons. +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Immutable { +public static partial class ImmutableEnumeratorExtensionMethods { + public static IImmutableEnumerator ToImmutableEnumerator(this IEnumerator e) + => ImmutableEnumerator.Make(e); + + public static IImmutableEnumerator GetImmutableEnumerator(this IEnumerable e) + => e.GetEnumerator().ToImmutableEnumerator(); + + public static Option>> FirstAndRest(this IImmutableEnumerator e) + => e.MoveNext() + .Match( + None: () => + Option.None>>(), + Some: elt => + new Tuple>( + elt.First, + elt.Rest + ).Some() + ); + + public static IEnumerable ToIEnumerable(this IImmutableEnumerator e) { + var next = e.MoveNext(); + while (next.IsSome) { + var elem = next.ElseThrow(new Exception("impossible")); + yield return elem.First; + next = elem.Rest.MoveNext(); + } + } + + [F] + private partial class TakeUntil_ { + public Option>> F( + ValueTuple /*e*/, IEqF, bool> /*predicate*/> t + ) + => t.Item2.F(t.Item1) + ? Option.None>>() + : t.Item1.MoveNext().IfSome(next => + new Tuple>( + next.First, + TakeUntil(next.Rest, t.Item2))); + } + + + public static IImmutableEnumerator TakeUntil(this IImmutableEnumerator e, IEqF, bool> predicate) + => new PureImmutableEnumerator< + ValueTuple< + IImmutableEnumerator, + IEqF, bool> + >, + T>( + (e, predicate), + TakeUntil_.Eq); + + [F] + private partial class Empty_ { + public Option>> F( + Unit _ + ) + => Option.None>>(); + } + + public static IImmutableEnumerator Empty() + => new PureImmutableEnumerator( + Unit.unit, + Empty_.Eq); + + [F] + private partial class ImSingleton_ { + public Option>> F( + T value + ) + => new Tuple>( + value, + Empty() + ).Some(); + } + + public static IImmutableEnumerator ImSingleton(this T value) + => new PureImmutableEnumerator( + value, + ImSingleton_.Eq); + + [F] + private partial class Concat_ { + public Option>> F( + ValueTuple, /*e1*/ IImmutableEnumerator /*e2*/> t + ) + => t.Item1.MoveNext().Match( + Some: element => + new Tuple>( + element.First, + element.Rest.Concat(t.Item2) + ).Some(), + None: () => + t.Item2.MoveNext().IfSome(element => + new Tuple>( + element.First, + element.Rest))); + } + + public static IImmutableEnumerator Concat(this IImmutableEnumerator e1, IImmutableEnumerator e2) + => new PureImmutableEnumerator< + ValueTuple< + IImmutableEnumerator /*e1*/, + IImmutableEnumerator /*e2*/ + >, + T>( + (e1, e2), + Concat_.Eq); + + [F] + private partial class Lazy_ { + public Option>> F( + ValueTuple> /*f*/> t + ) + => t.Item2.F(t.Item1).MoveNext().IfSome(element => + new Tuple>( + element.First, + element.Rest + )); + } + + // Apply a transformation to an immutable enumerator. + // The transformation function is only called when the + // result is stepped. It should only step its input + // enough to produce one element, but not more. + public static IImmutableEnumerator Lazy( + this IImmutableEnumerator e, + IEqF, IImmutableEnumerator> f) + => new PureImmutableEnumerator< + ValueTuple< + IImmutableEnumerator /*e*/, + IEqF, IImmutableEnumerator> /*f*/ + >, + U>( + (e, f), + Lazy_, U>.Eq); + + public static IImmutableEnumerator Lazy( + this IImmutableEnumerator e, + IEqF< + ValueTuple /*e*/, V /*v*/>, + IImmutableEnumerator + > f, + V v) + => new PureImmutableEnumerator< + ValueTuple< + ValueTuple /*e*/, V /*v*/>, + IEqF< + ValueTuple /*e*/, + V /*v*/>, + IImmutableEnumerator> /*f*/ + >, + U>( + ((e, v), f), + Lazy_, V>, U>.Eq); + + [F] + private partial class Flatten_ { + public IImmutableEnumerator F( + IImmutableEnumerator> e + ) + => e.MoveNext().Match( + Some: element => + element.First.Concat(element.Rest.Flatten()), + None: () => + Empty()); + } + + public static IImmutableEnumerator Flatten(this IImmutableEnumerator> e) + => e.Lazy(Flatten_.Eq); + + [F] + private partial class Select_ { + public IImmutableEnumerator F( + ValueTuple /*e*/, IEqF>, U> /*f*/> t + ) + => t.Item1.MoveNext().Match( + Some: element => + t.Item2.F((element.First, t.Item1)).ImSingleton().Concat( + element.Rest.Select(t.Item2)), + None: () => + Empty()); + } + + public static IImmutableEnumerator Select(this IImmutableEnumerator e, IEqF>, U> f) + => Lazy(e, Select_.Eq, f); + + [F] + private partial class SelectAggregate_ { + public IImmutableEnumerator F( + ValueTuple /*e*/, ValueTuple>, ValueTuple> /*f*/>> t + ) + { + var (e, accf) = t; + var (acc, f) = accf; + return e.MoveNext().Match( + Some: element => { + var res = f.F(acc, (element.First, e)); + var newAcc = res.Item1; + var result = res.Item2; + return result.ImSingleton().Concat( + element.Rest.SelectAggregate(newAcc, f)); + }, + None: () => + Empty()); + } + } + + public static IImmutableEnumerator SelectAggregate(this IImmutableEnumerator e, A acc, IEqF>, ValueTuple> f) + => Lazy(e, SelectAggregate_.Eq, (acc, f)); +} +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/IImmutableEnumerator.cs b/Utils/Immutable/Enumerator/IImmutableEnumerator.cs new file mode 100644 index 0000000..2d58f5a --- /dev/null +++ b/Utils/Immutable/Enumerator/IImmutableEnumerator.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Immutable { + public interface IImmutableEnumerator : IEquatable>, IEnumerable, IDisposable { + Option> MoveNext(); + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/IImmutableEnumeratorElement.cs b/Utils/Immutable/Enumerator/IImmutableEnumeratorElement.cs new file mode 100644 index 0000000..f3bc6bc --- /dev/null +++ b/Utils/Immutable/Enumerator/IImmutableEnumeratorElement.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Immutable { + public interface IImmutableEnumeratorElement : + IEquatable>, IDisposable { + T First { get; } + IImmutableEnumerator Rest { get; } + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/ImmutableEnumerator.cs b/Utils/Immutable/Enumerator/ImmutableEnumerator.cs new file mode 100644 index 0000000..e22b0bd --- /dev/null +++ b/Utils/Immutable/Enumerator/ImmutableEnumerator.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +// enumerator = { next: lazylist } +// enumeratorElement = { +// current: U; +// next: lazylist; +// } +// state = AlreadyUnfoldedEnd +// | AlreadyUnfolded of ImmutableEnumeratorElement +// | NotUnfoldedYet of IEnumerator +// lazylist = state ref + +namespace Immutable { + // enumerator = { next: lazylist } + public partial class ImmutableEnumerator : IImmutableEnumerator { + private readonly LazyList next; // readonly + private readonly Last last; // readonly + private readonly int hashCode; + + private ImmutableEnumerator(LazyList next, Last last) { + this.next = next; + this.last = last; + // Use the default hashCode on the single mutable LazyList + // instance for this position in the enumerator. + this.hashCode = next.GetHashCode(); + } + + public void Dispose() { + // Calling this method on any copy of any immutable + // enumerator, including via a using(){} directive + // is DANGEROUS: it will make it impossible to enumerate + // past the current position of the underlying enumerator, + // when starting from any copy of any immutable enumerator + // for that underlying enumerator. + // As a precaution, and to catch bugs early, we make it + // impossible to use any of the copies of any immutable + // enumerator for that underlying enumerator once this method + // has been called. + last.LAST.CallDispose(); + last.EXPLICITLY_DISPOSED = true; + } + + public static ImmutableEnumerator Make(IEnumerator e) { + var last = new Last { LAST = null }; + var lst = new LazyList { + NEXT = new State.NotUnfoldedYet(e, last) + }; + last.LAST = lst; + return new ImmutableEnumerator(lst, last); + } + + public Option> MoveNext() { + if (this.last.EXPLICITLY_DISPOSED) { + throw new ObjectDisposedException("Cannot use an ImmutableEnumerator after it was explicitly disposed."); + } + return next.NEXT.Match( + AlreadyUnfoldedEnd: () => + Option.None>(), + AlreadyUnfolded: element => + element.Some(), + NotUnfoldedYet: (e, last) => { + if (e.MoveNext()) { + var lst = new LazyList { + NEXT = new State.NotUnfoldedYet(e, last) + }; + last.LAST = lst; + var elem = new ImmutableEnumeratorElement( + current: e.Current, + next: lst, + last : last); + next.NEXT = new State.AlreadyUnfolded(elem); + return elem.Some(); + } else { + next.NEXT = new State.AlreadyUnfoldedEnd(); + // Call .Dispose() on the underlying enumerator + // because we have read all its elements. + e.Dispose(); + return Option.None>(); + } + } + ); + } + + public static bool operator ==(ImmutableEnumerator a, ImmutableEnumerator b) + => Equality.Operator(a, b); + public static bool operator !=(ImmutableEnumerator a, ImmutableEnumerator b) + => !(a == b); + public override bool Equals(object other) + => Equality.Untyped>( + this, + other, + x => x as ImmutableEnumerator, + x => x.hashCode, + // Two immutable enumerators are equal if and only if + // they are at the same position and use the same + // underlying enumerable. In that case they are guaranteed + // to behave identically to an outside observer (except for + // side-effects caused by the iteration of the underlying + // enumerator, which only occur on the first .MoveNext() + // call, if it is called on several equal immutable + // enumerators). This is also true for the + // ImmutableEnumeratorElement subclass, because if two of + // these have the same underlying generator, their current + // field are necessarily one and the same. + (x, y) => Object.ReferenceEquals(x.next, y.next)); + public bool Equals(IImmutableEnumerator other) + => Equality.Equatable>(this, other); + public override int GetHashCode() => hashCode; + public override string ToString() => "ImmutableEnumerator"; + + public IEnumerator GetEnumerator() + => this.ToIEnumerable().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + => this.ToIEnumerable().GetEnumerator(); + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/ImmutableEnumeratorElement.cs b/Utils/Immutable/Enumerator/ImmutableEnumeratorElement.cs new file mode 100644 index 0000000..cda7e18 --- /dev/null +++ b/Utils/Immutable/Enumerator/ImmutableEnumeratorElement.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +// enumerator = { next: lazylist } +// enumeratorElement = { +// current: U; +// next: lazylist; +// } +// state = AlreadyUnfoldedEnd +// | AlreadyUnfolded of ImmutableEnumeratorElement +// | NotUnfoldedYet of IEnumerator +// lazylist = state ref + +namespace Immutable { + // enumerator = { next: lazylist } + public partial class ImmutableEnumerator : IImmutableEnumerator { + // enumeratorElement = { + // current: U; + // next: lazylist; + // } + private sealed class ImmutableEnumeratorElement : IImmutableEnumeratorElement { + private readonly U current; // immutable + private readonly ImmutableEnumerator rest; + private readonly int hashCode; + + public ImmutableEnumeratorElement(U current, LazyList next, Last last) { + this.current = current; + this.rest = new ImmutableEnumerator(next, last); + this.hashCode = Equality.HashCode(current, rest); + } + + public U First { + get { + if (this.rest.last.EXPLICITLY_DISPOSED) { + throw new ObjectDisposedException("Cannot use an ImmutableEnumerator after it was explicitly disposed."); + } + return current; + } + } + + public IImmutableEnumerator Rest { get => rest; } + + public void Dispose() { + // Calling this method on any copy of any immutable + // enumerator, including via a using(){} directive + // is DANGEROUS: it will make it impossible to enumerate + // past the current position of the underlying enumerator, + // when starting from any copy of any immutable enumerator + // for that underlying enumerator. + // As a precaution, and to catch bugs early, we make it + // impossible to use any of the copies of any immutable + // enumerator for that underlying enumerator once this method + // has been called. + rest.last.LAST.CallDispose(); + rest.last.EXPLICITLY_DISPOSED = true; + } + + public Option> MoveNext() + => rest.MoveNext(); + + public static bool operator ==(ImmutableEnumeratorElement a, ImmutableEnumeratorElement b) + => Equality.Operator(a, b); + public static bool operator !=(ImmutableEnumeratorElement a, ImmutableEnumeratorElement b) + => !(a == b); + public override bool Equals(object other) + => Equality.Untyped( + this, + other, + x => x as ImmutableEnumeratorElement, + x => x.hashCode, + // Two immutable enumerators are equal if and only if + // they are at the same position and use the same + // underlying enumerable. In that case they are guaranteed + // to behave identically to an outside observer (except for + // side-effects caused by the iteration of the underlying + // enumerator, which only occur on the first .MoveNext() + // call, if it is called on several equal immutable + // enumerators). This is also true for the + // ImmutableEnumeratorElement subclass, because if two of + // these have the same underlying generator, their current + // field are necessarily one and the same. + x => x.current, + x => x.rest); + public bool Equals(IImmutableEnumeratorElement other) + => Equality.Equatable>(this, other); + public override int GetHashCode() => hashCode; + public override string ToString() => "ImmutableEnumeratorElement"; + } + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/Last.cs b/Utils/Immutable/Enumerator/Last.cs new file mode 100644 index 0000000..8882183 --- /dev/null +++ b/Utils/Immutable/Enumerator/Last.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +// enumerator = { next: lazylist } +// enumeratorElement = { +// current: U; +// next: lazylist; +// } +// state = AlreadyUnfoldedEnd +// | AlreadyUnfolded of ImmutableEnumeratorElement +// | NotUnfoldedYet of IEnumerator +// lazylist = state ref + +namespace Immutable { + // enumerator = { next: lazylist } + public partial class ImmutableEnumerator : IImmutableEnumerator { + private class Last { + // This is one of the three mutable fields in this file. + // It is used to update the pointer to the only + // NotUnfoldedYet object for a given underlying enumerator. + // This allows a call on .Dispose() to clean up after the + // underlying enumerator. + public LazyList LAST; + // This is one of the three mutable fields in this file. + // It is used to indicate that the .Dispose() method has + // been called and that it is therefore unsafe to continue + // using the immutable enumerator for this underlying + // enumerator. + public bool EXPLICITLY_DISPOSED = false; + } + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/LazyList.cs b/Utils/Immutable/Enumerator/LazyList.cs new file mode 100644 index 0000000..3276c9d --- /dev/null +++ b/Utils/Immutable/Enumerator/LazyList.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +// enumerator = { next: lazylist } +// enumeratorElement = { +// current: U; +// next: lazylist; +// } +// state = AlreadyUnfoldedEnd +// | AlreadyUnfolded of ImmutableEnumeratorElement +// | NotUnfoldedYet of IEnumerator +// lazylist = state ref + +namespace Immutable { + // enumerator = { next: lazylist } + public partial class ImmutableEnumerator : IImmutableEnumerator { + // lazylist = state ref + private class LazyList { + // This is one of the three mutable fields in this file. + // There is one LazyList object created each time + // the underlying enumerator's MoveNext() method + // is called, except for the last failed MoveNext() + // call. There is also one initial LazyList element + // created when wrapping the underlying enumerator + // to create an immutable enumerator. + public State NEXT; // mutable + + ~LazyList() { this.CallDispose(); } + public void CallDispose() { + NEXT.Match( + AlreadyUnfoldedEnd: () => Unit.unit, + AlreadyUnfolded: _e => Unit.unit, + // Since at any one time there is only one LazyList + // whose state is NotUnfoldedYet (except during the + // unfolding, when there two such lists manipulated for + // a brief time by the same lambda), the enumerator + // should be disposed of when the destructor of this + // class is called. + NotUnfoldedYet: (r, last) => { + r.Dispose(); + return Unit.unit; + } + ); + } + } + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/PureImmutableEnumerator.cs b/Utils/Immutable/Enumerator/PureImmutableEnumerator.cs new file mode 100644 index 0000000..b0c9a41 --- /dev/null +++ b/Utils/Immutable/Enumerator/PureImmutableEnumerator.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Immutable { + public class PureImmutableEnumerator : IImmutableEnumerator { + private T state; + private IEqF>>> generator; + private int hashCode; + + public PureImmutableEnumerator(T state, IEqF>>> generator) { + this.state = state; + this.generator = generator; + this.hashCode = Equality.HashCode("PureImmutableEnumerator", state, generator); + } + + public static bool operator ==(PureImmutableEnumerator a, PureImmutableEnumerator b) + => Equality.Operator(a, b); + public static bool operator !=(PureImmutableEnumerator a, PureImmutableEnumerator b) + => !(a == b); + public override bool Equals(object other) + => Equality.Untyped( + this, + other, + x => x as PureImmutableEnumerator, + x => x.hashCode, + // Two immutable enumerators are equal if and only if + // they have the same (immutable) state and use the same + // generator lambda. + x => x.state, + x => x.generator); + public bool Equals(IImmutableEnumerator other) + => Equality.Equatable>(this, other); + public override int GetHashCode() => hashCode; + public override string ToString() => "ImmutableEnumerator"; + + public IEnumerator GetEnumerator() + => this.ToIEnumerable().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + => this.ToIEnumerable().GetEnumerator(); + + void IDisposable.Dispose() { /* Nothing to do */ } + + public Option> MoveNext() + => generator.F(state).IfSome((first, rest) => + new PureImmutableEnumeratorElement(first, rest)); + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/PureImmutableEnumeratorElement.cs b/Utils/Immutable/Enumerator/PureImmutableEnumeratorElement.cs new file mode 100644 index 0000000..d5f9973 --- /dev/null +++ b/Utils/Immutable/Enumerator/PureImmutableEnumeratorElement.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Immutable { + public class PureImmutableEnumeratorElement : IImmutableEnumeratorElement { + private U element; + private IImmutableEnumerator rest; + private int hashCode; + + public PureImmutableEnumeratorElement(U element, IImmutableEnumerator rest) { + this.element = element; + this.rest = rest; + this.hashCode = Equality.HashCode("PureImmutableEnumeratorElement", element, rest); + } + + public static bool operator ==(PureImmutableEnumeratorElement a, PureImmutableEnumeratorElement b) + => Equality.Operator(a, b); + public static bool operator !=(PureImmutableEnumeratorElement a, PureImmutableEnumeratorElement b) + => !(a == b); + public override bool Equals(object other) + => Equality.Untyped>( + this, + other, + x => x as PureImmutableEnumeratorElement, + x => x.hashCode, + // Two immutable enumerators are equal if and only if + // they have the same (immutable) state and use the same + // generator lambda. + x => x.element, + x => x.rest); + public bool Equals(IImmutableEnumeratorElement other) + => Equality.Equatable>(this, other); + public override int GetHashCode() => hashCode; + public override string ToString() => "PureImmutableEnumeratorElement"; + + public void Dispose() { /* Nothing to do */ } + + public U First { get => element; } + + public IImmutableEnumerator Rest { get => rest; } + + public Option> MoveNext() + => rest.MoveNext(); + } +} \ No newline at end of file diff --git a/Utils/Immutable/Enumerator/State.cs b/Utils/Immutable/Enumerator/State.cs new file mode 100644 index 0000000..cc8647e --- /dev/null +++ b/Utils/Immutable/Enumerator/State.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +// enumerator = { next: lazylist } +// enumeratorElement = { +// current: U; +// next: lazylist; +// } +// state = AlreadyUnfoldedEnd +// | AlreadyUnfolded of ImmutableEnumeratorElement +// | NotUnfoldedYet of IEnumerator +// lazylist = state ref + +namespace Immutable { + // enumerator = { next: lazylist } + public partial class ImmutableEnumerator : IImmutableEnumerator { + // state = AlreadyUnfoldedEnd + // | AlreadyUnfolded of ImmutableEnumeratorElement + // | NotUnfoldedYet of IEnumerator + private abstract class State { + public abstract T Match( + Func AlreadyUnfolded, + Func AlreadyUnfoldedEnd, + Func, Last, T> NotUnfoldedYet); + + public class AlreadyUnfolded : State { + private readonly ImmutableEnumeratorElement value; + public AlreadyUnfolded(ImmutableEnumeratorElement value) { + this.value = value; + } + public override T Match( + Func AlreadyUnfolded, + Func AlreadyUnfoldedEnd, + Func, Last, T> NotUnfoldedYet) + => AlreadyUnfolded(value); + } + + public class AlreadyUnfoldedEnd : State { + public AlreadyUnfoldedEnd() {} + public override T Match( + Func AlreadyUnfolded, + Func AlreadyUnfoldedEnd, + Func, Last, T> NotUnfoldedYet) + => AlreadyUnfoldedEnd(); + } + + public class NotUnfoldedYet : State { + private readonly IEnumerator enumerator; + private readonly Last last; // readonly + public NotUnfoldedYet(IEnumerator enumerator, Last last) { + this.enumerator = enumerator; + this.last = last; + } + public override T Match( + Func AlreadyUnfolded, + Func AlreadyUnfoldedEnd, + Func, Last, T> NotUnfoldedYet) + => NotUnfoldedYet(enumerator, last); + } + } + } +} \ No newline at end of file diff --git a/Utils/Immutable/EnumeratorGenerator.cs b/Utils/Immutable/EnumeratorGenerator.cs new file mode 100644 index 0000000..8691081 --- /dev/null +++ b/Utils/Immutable/EnumeratorGenerator.cs @@ -0,0 +1,20 @@ +using static Generator; + +public static class EnumeratorGenerator { + public static void Main() { + Generate( + "Utils/Immutable/EnumeratorGenerated.cs", + "", + "namespace Immutable {", + "}", + "Immutable.", + Types( +// Our boilerplate generator does not support +// defining generic types for now. +/* + Record("PureImmutableGenerator", + Field("T", "state"), + Field("Func>>>", "generator")) + */)); + } +} diff --git a/Utils/Immutable/EquatableDictionary.cs b/Utils/Immutable/EquatableDictionary.cs new file mode 100644 index 0000000..e7865a1 --- /dev/null +++ b/Utils/Immutable/EquatableDictionary.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Collections.Immutable; + +public class EquatableDictionary : IEnumerable>, IString, IEquatable> { + public readonly ImmutableDictionary dictionary; + + public EquatableDictionary() { + this.dictionary = ImmutableDictionary.Empty; + this.hashCode = "EquatableDictionary".GetHashCode(); + } + + public EquatableDictionary(ImmutableDictionary dictionary) { + this.dictionary = dictionary; + this.hashCode = dictionary.Aggregate( + "EquatableDictionary".GetHashCode(), + (h, kvp) => + h ^ kvp.Key.GetHashCode() ^ kvp.Value.GetHashCode()); + } + + private EquatableDictionary(EquatableDictionary dictionary, TKey key, TValue value) { + this.dictionary = dictionary.dictionary.Add(key, value); + this.hashCode = + dictionary.hashCode ^ key.GetHashCode() ^ value.GetHashCode(); + } + + public TValue this[TKey key] { + get => dictionary[key]; + } + + public EquatableDictionary Add(TKey key, TValue value) + => new EquatableDictionary(this, key, value); + + // These would need to update the hashCode, disabled for now. + /*public EquatableDictionary SetItem(TKey key, TValue value) + => new EquatableDictionary(dictionary.SetItem(key, value)); + + public EquatableDictionary Remove(TKey key) + => new EquatableDictionary(dictionary.Remove(key));*/ + + public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => dictionary.GetEnumerator(); + + /*public EquatableDictionaryLens< + TKey, + TValue, + EquatableDictionary> + lens { + get => this.ChainLens(x => x); + }*/ + + public override string ToString() + => "EquatableDictionary {\n" + + this.Select(kvp => (ks: kvp.Key.ToString(), + vs: kvp.Value.ToString())) + .OrderBy(p => p.ks) + .Select(p => $"{{ {p.ks}, {p.vs} }}") + .JoinWith(",\n") + + "\n}"; + + public string Str() => ToString(); + + private bool SameKVP(EquatableDictionary other) { + foreach (var kvp in this) { + // Let's hope that this uses EqualityComparer.Default and EqualityComparer.Default. + if (!other.Contains(kvp)) { return false; } + } + foreach (var kvp in this) { + if (!this.Contains(kvp)) { return false; } + } + return false; + } + + public override bool Equals(object other) + => Equality.Untyped>( + this, + other, + x => x as EquatableDictionary, + x => x.hashCode, + (x, y) => x.SameKVP(y)); + public bool Equals(EquatableDictionary other) + => Equality.Equatable>(this, other); + private readonly int hashCode; + public override int GetHashCode() => hashCode; +} + +public static class EquatableDictionaryExtensionMethods { + public static EquatableDictionary ToEquatableDictionary(this IEnumerable e, Func key, Func value) + => new EquatableDictionary(e.ToImmutableDictionary(key, value)); + + public static EquatableDictionary ToEquatableDictionary(this ImmutableDictionary d) + => new EquatableDictionary(d); +} \ No newline at end of file diff --git a/Utils/Immutable/Option.cs b/Utils/Immutable/Option.cs index ef4b1bd..cd99719 100644 --- a/Utils/Immutable/Option.cs +++ b/Utils/Immutable/Option.cs @@ -64,6 +64,9 @@ namespace Immutable { public static Option IfSome(this Option o, Func some) => o.Map(some); + public static Option IfSome(this Option> o, Func some) + => o.Map(o1o2 => some(o1o2.Item1, o1o2.Item2)); + public static Option Bind(this Option o, Func> f) => o.Match_(Some: some => f(some), None: () => Option.None()); @@ -83,5 +86,9 @@ namespace Immutable { public static T ElseThrow(this Option o, Func none) => o.Match_(Some: value => value, None: () => throw none()); + + public static T ElseThrow(this Option o, Exception none) + => o.Match_(Some: value => value, + None: () => throw none); } } \ No newline at end of file diff --git a/Utils/Immutable/Rope.cs b/Utils/Immutable/Rope.cs new file mode 100644 index 0000000..5879d8a --- /dev/null +++ b/Utils/Immutable/Rope.cs @@ -0,0 +1,33 @@ +using System.Text; +using System.Collections.Immutable; + +namespace Immutable { + public partial class Node { + private string CustomToString() { + var sb = new StringBuilder(); + var stack = ImmutableStack.Empty; + while (!stack.IsEmpty) { + var e = stack.Peek(); + stack = stack.Pop(); + e.Match( + Leaf: s => { + sb.Append(s); + return Unit.unit; + }, + Node: x => { + stack = stack.Push(x.a).Push(x.b); + return Unit.unit; + }); + } + return sb.ToString(); + } + + // TODO: this is not used by the generated code + private int CustomHashCode(Node a, Node b) + => a.GetHashCode() ^ b.GetHashCode(); + + private bool CustomEquals(Node a, Node b) + // TODO: a faster implementation of equality + => a.ToString().Equals(b.ToString()); + } +} \ No newline at end of file diff --git a/Utils/Immutable/RopeGenerator.cs b/Utils/Immutable/RopeGenerator.cs new file mode 100644 index 0000000..7671792 --- /dev/null +++ b/Utils/Immutable/RopeGenerator.cs @@ -0,0 +1,19 @@ +using static Generator; + +public static class RopeGenerator { + public static void Main() { + Generator.Generate( + "Utils/Immutable/RopeGenerated.cs", + "", + "namespace Immutable {", + "}", + "Immutable.", + Types( + Variant("Rope", + Case("string", "Leaf"), + Case("Immutable.Node", "Node")), + Record("Node", + Field("Rope", "a"), + Field("Rope", "b")))); + } +} \ No newline at end of file