diff --git a/DefaultGrammar.cs b/DefaultGrammar.cs new file mode 100644 index 0000000..0a7face --- /dev/null +++ b/DefaultGrammar.cs @@ -0,0 +1,14 @@ +using S = Lexer.S; +using PrecedenceDAG = ImmutableDefaultDictionary; +using static Global; +using static MixFix; +using static MixFix.Associativity; + +public static class DefaultGrammar { + public static PrecedenceDAG DefaultPrecedenceDAG + = EmptyPrecedenceDAG + .WithOperator("bool", NonAssociative, "equality|terminal", S.And, "equality|terminal") + .WithOperator("equality", NonAssociative, "int", S.Eq, "int") + .WithOperator("int", NonAssociative, S.Int) + .WithOperator("terminal", NonAssociative, S.Ident); +} \ No newline at end of file diff --git a/Exceptions.cs b/Exceptions.cs index 6b2f463..4a92770 100644 --- a/Exceptions.cs +++ b/Exceptions.cs @@ -8,6 +8,10 @@ public class ParserErrorException : UserErrorException { public ParserErrorException(string e) : base("Parser error: " + e) {} } +public class ParserExtensionException : UserErrorException { + public ParserExtensionException(string e) : base("Parser extension error: " + e) {} +} + public class LexerErrorException : UserErrorException { public LexerErrorException(string e) : base("Lexer error: " + e) {} } diff --git a/Lexer.cs b/Lexer.cs index fd30f40..2dd0c30 100644 --- a/Lexer.cs +++ b/Lexer.cs @@ -63,6 +63,25 @@ public static partial class Lexer { Rule(S.Space, C.SpaceSeparator, S.Space), Rule(S.Space, EOF, S.End), Rule(S.Space, '"', S.StringOpen, S.String), + Rule(S.Space, new[]{'='}, S.Eq), + Rule(S.Eq, new[]{'='}, S.Eq, S.Space), + Rule(S.Space, new[]{'&'}, S.And), + Rule(S.And, new[]{'&'}, S.And, S.Space), + // TODO: https://unicode.org/reports/tr31/#D1 has a lot to say about + // identifiers + Rule(S.Space, C.LowercaseLetter, S.Ident), + Rule(S.Space, C.UppercaseLetter, S.Ident), + Rule(S.Space, C.TitlecaseLetter, S.Ident), + Rule(S.Space, C.ModifierLetter, S.Ident), + Rule(S.Space, C.OtherLetter, S.Ident), + Rule(S.Space, C.LetterNumber, S.Ident), + + Rule(S.Ident, C.LowercaseLetter, S.Ident), + Rule(S.Ident, C.UppercaseLetter, S.Ident), + Rule(S.Ident, C.TitlecaseLetter, S.Ident), + Rule(S.Ident, C.ModifierLetter, S.Ident), + Rule(S.Ident, C.OtherLetter, S.Ident), + Rule(S.Ident, C.LetterNumber, S.Ident), Rule(S.Int, C.DecimalDigitNumber, S.Int), Rule(S.Int, C.SpaceSeparator, S.Space), diff --git a/LexerGenerator.cs b/LexerGenerator.cs index 6cbead5..5d50d35 100644 --- a/LexerGenerator.cs +++ b/LexerGenerator.cs @@ -14,6 +14,8 @@ public static class LexerGenerator { Case("Space"), Case("Int"), Case("Decimal"), + Case("Ident"), + Case("And"), Case("Eq"), Case("Space"), Case("String"), diff --git a/Makefile b/Makefile index 451ee5b..359d707 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,6 @@ run: main.exe Makefile main.exe: $(CS) $(GENERATED) Makefile mcs -debug+ -out:$@ \ - /reference:/usr/lib/mono/fsharp/FSharp.Core.dll \ /reference:/usr/lib/mono/4.5/System.Collections.Immutable.dll \ /reference:/usr/lib/mono/4.5/Facades/netstandard.dll \ $(filter-out Makefile, $^) diff --git a/MixFix.cs b/MixFix.cs new file mode 100644 index 0000000..07f74c4 --- /dev/null +++ b/MixFix.cs @@ -0,0 +1,240 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Immutable; +using S = Lexer.S; +using Lexeme = Lexer.Lexeme; +using PrecedenceDAG = ImmutableDefaultDictionary; +using PrecedenceGroupName = System.String; +using Hole = System.Collections.Immutable.ImmutableHashSet< + string /* PrecedenceGroupName */>; +using static Global; +using static MixFix.Fixity; + +public class Foo { + public string v; + public static implicit operator Foo(string s) => new Foo{v=s}; + public static Foo operator |(Foo a, string b) => a.v + b; +} + +public static partial class MixFix { + public partial class Operator { + private string CustomToString() + => $"Operator(\"{precedenceGroup}\", {fixity}, {parts.Select(x => x.Match(Hole: h => h.Select(g => g.ToString()).JoinWith("|"), Name: n => $"\"{n}\"")).JoinWith(", ")})"; + } + + public abstract partial class Part { + public static implicit operator Part(S s) + => Part.Name(s); + public static implicit operator Part(string s) + => Part.Hole(s.Split("|").ToImmutableHashSet()); + } + + public partial class Fixity { + // Used in the fixity table below. + public static implicit operator Func(Fixity fixity) => () => fixity; + } + + public partial class Operator { + // TODO: cache this for improved complexity + public Option> leftmostHole { + get => parts.First().Bind(firstPart => firstPart.AsHole); + } + + // TODO: cache this for improved complexity + public Option> rightmostHole { + get => parts.Last().Bind(lastPart => lastPart.AsHole); + } + + private static Func error = + () => throw new Exception("Internal error: unexpected fixity."); + + private static Func notYet = + () => throw new NotImplementedException( + "NonAssociative prefix and postfix operators are not supported for now."); + + private ImmutableList>> fixityTable = new [] { + // neither start end both ←Hole//↓Associativity + new[]{ Closed, notYet, notYet, InfixNonAssociative },//NonAssociative + new[]{ error, Postfix, error, InfixLeftAssociative },//LeftAssociative + new[]{ error, error, Prefix, InfixRightAssociative },//RightAssociative + }.Select(ImmutableList.ToImmutableList).ToImmutableList(); + + public Fixity fixity { + get { + var startsWithHole = parts.First() + .Bind(lastPart => lastPart.AsHole) + .IsSome; + + var endsWithHole = parts.First() + .Bind(lastPart => lastPart.AsHole) + .IsSome; + + var row = associativity.Match( + NonAssociative: () => 0, + LeftAssociative: () => 1, + RightAssociative: () => 2); + + var column = ((!startsWithHole) && (!endsWithHole)) ? 0 + : (( startsWithHole) && (!endsWithHole)) ? 1 + : ((!startsWithHole) && ( endsWithHole)) ? 2 + : (( startsWithHole) && ( endsWithHole)) ? 3 + : throw new Exception("impossible"); + + return fixityTable[row][column](); + } + } + } + + public partial class DAGNode { + // TODO: cache this for improved complexity + public IEnumerable allOperators { + get => ImmutableList( + closed, + prefix, + postfix, + infixNonAssociative, + infixRightAssociative, + infixLeftAssociative + ).SelectMany(x => x); + } + + // TODO: cache this for improved complexity + public Option> leftmostHole { + get => allOperators.First(@operator => @operator.leftmostHole); + } + + // TODO: cache this for improved complexity + public Option> rightmostHole { + get => allOperators.First(@operator => @operator.rightmostHole); + } + } + + public static DAGNode EmptyDAGNode = new DAGNode( + closed: ImmutableList.Empty, + prefix: ImmutableList.Empty, + postfix: ImmutableList.Empty, + infixNonAssociative: ImmutableList.Empty, + infixRightAssociative: ImmutableList.Empty, + infixLeftAssociative: ImmutableList.Empty + ); + + public static PrecedenceDAG EmptyPrecedenceDAG + = new PrecedenceDAG(EmptyDAGNode); + + public static Whole Add(this ILens node, Operator @operator) { + return @operator.fixity.Match( + Closed: + () => node.Closed(), + Prefix: + () => node.Prefix(), + Postfix: + () => node.Postfix(), + InfixNonAssociative: + () => node.InfixNonAssociative(), + InfixRightAssociative: + () => node.InfixRightAssociative(), + InfixLeftAssociative: + () => node.InfixLeftAssociative() + ).Cons(@operator); + } + + public static void CheckHole(PrecedenceDAG precedenceDAG, Operator @operator, string name, Option existing, Option @new) { + existing.IfSome(existingHole => + @new.IfSome(newHole => { + if (! newHole.SetEquals(existingHole)) { + throw new ParserExtensionException($"Cannot extend parser with operator {@operator}, its {name} hole ({newHole.ToString()}) must either be empty or else use the same precedence groups as the existing operators in {@operator.precedenceGroup}, i.e. {existingHole}."); + } + return unit; + }) + ); + } + + public static void CheckLeftmostHole(PrecedenceDAG precedenceDAG, Operator @operator) + => CheckHole( + precedenceDAG, + @operator, + "leftmost", + precedenceDAG[@operator.precedenceGroup].leftmostHole, + @operator.leftmostHole); + + public static void CheckRightmostHole(PrecedenceDAG precedenceDAG, Operator @operator) + => CheckHole( + precedenceDAG, + @operator, + "rightmost", + precedenceDAG[@operator.precedenceGroup].rightmostHole, + @operator.rightmostHole); + + public static void CheckNonEmpty(PrecedenceDAG precedenceDAG, Operator @operator) { + if (@operator.parts.Count == 0) { + throw new ParserExtensionException($"Cannot extend parser with operator {@operator}, it has no parts."); + } + } + + public static void CheckConsecutiveHoles(PrecedenceDAG precedenceDAG, Operator @operator) { + @operator.parts.Aggregate((previous, current) => { + if (previous.IsHole && current.IsHole) { + throw new ParserExtensionException($"Cannot extend parser with operator {@operator}, it has two consecutive holes. This is not supported for now."); + } + return current; + }); + } + + public static void CheckSingleUseOfNames(PrecedenceDAG precedenceDAG, Operator @operator) { + // TODO: check that each name part isn't used elsewhere in a way that could cause ambiguity (use in a different namespace which is bracketed is okay, etc.). Probably something to do with paths reachable from the root. + } + + public static void CheckNotAlias(PrecedenceDAG precedenceDAG, Operator @operator) { + if (@operator.parts.Single().Bind(part => part.AsHole).IsSome) { + throw new ParserExtensionException($"Cannot extend parser with operator {@operator}, it only contains a single hole. Aliases built like this are not supported for now."); + } + } + + public static PrecedenceDAG With(this PrecedenceDAG precedenceDAG, Operator @operator) { + // This is where all the checks are done to ensure that the + // resulting grammar is well-formed and assuredly unambiguous. + // Future extension idea: add an "ambiguous" keyword to + // alleviate some restrictions. + CheckLeftmostHole(precedenceDAG, @operator); + CheckRightmostHole(precedenceDAG, @operator); + CheckNonEmpty(precedenceDAG, @operator); + CheckConsecutiveHoles(precedenceDAG, @operator); + CheckSingleUseOfNames(precedenceDAG, @operator); + CheckNotAlias(precedenceDAG, @operator); + return precedenceDAG.lens[@operator.precedenceGroup].Add(@operator); + } + + public static PrecedenceDAG WithOperator(this PrecedenceDAG precedenceDAG, string precedenceGroup, Associativity associativity, params Part[] parts) + => precedenceDAG.With( + new Operator( + precedenceGroup: precedenceGroup, + associativity: associativity, + parts: parts.ToImmutableList())); + + public static Grammar OperatorToGrammar(Operator @operator) { + //@operator. + throw new NotImplementedException(); + } + + public static Grammar DAGNodeToGrammar(DAGNode node) { +// return Grammar.Or(ImmutableList( +// node.closed +// closed, +// succl nonassoc succr, +// (prefix | succl rightassoc)+ succr, +// succl (suffix | succr rightassoc)+ +// )); + throw new NotImplementedException(); + } + + public static void DAGToGrammar(DAGNode precedenceDAG) { + + } + + public static void RecursiveDescent(IEnumerable e) { + + } +} \ No newline at end of file diff --git a/MixFixGenerator.cs b/MixFixGenerator.cs new file mode 100644 index 0000000..964f6a4 --- /dev/null +++ b/MixFixGenerator.cs @@ -0,0 +1,48 @@ +using static Generator; + +public static class ParserGenerator { + public static void Main() { + Generate( + "MixFixGenerated.cs", + "using System.Collections.Immutable;\n" + + "using S = Lexer.S;\n" + + "using PrecedenceGroupName = System.String;", + "public static partial class MixFix {", + "}", + "MixFix.", + Types( + Variant("Grammar", + Case("ImmutableList", "Or"), + Case("ImmutableList", "Sequence")), + + Variant("Fixity", + Case("Closed"), + Case("InfixLeftAssociative"), + Case("InfixRightAssociative"), + Case("InfixNonAssociative"), + Case("Prefix"), + Case("Postfix")), + + Variant("Associativity", + Case("NonAssociative"), + Case("LeftAssociative"), + Case("RightAssociative")), + + Record("Operator", + Field("PrecedenceGroupName", "precedenceGroup"), + Field("Associativity", "associativity"), + Field("ImmutableList", "parts")), + + Variant("Part", + Case("S", "Name"), + Case("ImmutableHashSet", "Hole")), + + Record("DAGNode", + Field("ImmutableList", "infixLeftAssociative"), + Field("ImmutableList", "infixRightAssociative"), + Field("ImmutableList", "infixNonAssociative"), + Field("ImmutableList", "prefix"), + Field("ImmutableList", "postfix"), + Field("ImmutableList", "closed")))); + } +} \ No newline at end of file diff --git a/Parser.cs b/Parser.cs index a80a7a0..30431ca 100644 --- a/Parser.cs +++ b/Parser.cs @@ -10,16 +10,19 @@ using static Global; public static partial class Parser { public static Ast.Expr Parse(string source) { + Log(DefaultGrammar.DefaultPrecedenceDAG.ToString()); return Lexer.Lex(source) .SelectMany(lexeme => lexeme.state.Match( Int: () => Ast.Expr.Int(Int32.Parse(lexeme.lexeme)).Singleton(), String: () => Ast.Expr.String(lexeme.lexeme).Singleton(), + Ident: () => Enumerable.Empty(), // TODO + And: () => Enumerable.Empty(), // TODO Space: () => Enumerable.Empty(), // ignore - Eq: () => Enumerable.Empty(), - End: () => Enumerable.Empty(), - Decimal: () => Enumerable.Empty(), - StringOpen: () => Enumerable.Empty(), + Eq: () => Enumerable.Empty(), // TODO + End: () => Enumerable.Empty(), // TODO + Decimal: () => Enumerable.Empty(), // TODO + StringOpen: () => Enumerable.Empty(), // TODO StringClose: () => Enumerable.Empty() ) ) diff --git a/T4/Generator.cs b/T4/Generator.cs index a7fe0dd..8c4749e 100644 --- a/T4/Generator.cs +++ b/T4/Generator.cs @@ -14,10 +14,11 @@ public static class Generator { public static void Generate(string outputFile, string singleHeader, string header, string footer, string qualifier, ImmutableDictionary>> types) { using (var o = new System.IO.StreamWriter(outputFile)) { Action w = o.WriteLine; - w("// This file was generated by Generator.cs"); + w("// This file was generated by T4/Generator.cs"); w(""); w("using System;"); + w("using Immutable;"); w($"{singleHeader}"); foreach (var type in types) { var name = type.Key; diff --git a/T4/RecordGenerator.cs b/T4/RecordGenerator.cs index c0ecdcd..d697c8a 100644 --- a/T4/RecordGenerator.cs +++ b/T4/RecordGenerator.cs @@ -55,6 +55,14 @@ public static class RecordGenerator { w($" );"); } + private static void StringConversion(this Action w, string qualifier, string name, Record record) { + w($" public override string ToString()"); + w($" => this.CustomToString();"); + w($" private string CustomToString(params Immutable.Uninstantiatable[] _)"); + w($" => $\"{name}(\\n{String.Join(",\\n", record.Select(@field => $" {@field.Key}: {{{@field.Key}.Str<{@field.Value}>()}}"))})\";"); + w($" public string Str() => ToString();"); + } + private static void With(this Action w, string qualifier, string name, Record record) { foreach (var @field in record) { var F = @field.Key; @@ -101,13 +109,15 @@ public static class RecordGenerator { } private static void RecordClass(this Action w, string qualifier, string name, Record record) { - w($" public sealed class {name} : IEquatable<{name}> {{"); + w($" public sealed partial class {name} : IEquatable<{name}>, IString {{"); w.Fields(qualifier, name, record); w($""); w.Constructor(qualifier, name, record); w($""); w.Equality(qualifier, name, record); w($""); + w.StringConversion(qualifier, name, record); + w($""); w.With(qualifier, name, record); w($""); w.Lens(qualifier, name, record); diff --git a/T4/VariantGenerator.cs b/T4/VariantGenerator.cs index bd86981..76ae191 100644 --- a/T4/VariantGenerator.cs +++ b/T4/VariantGenerator.cs @@ -1,4 +1,4 @@ -// Code quality of this file: medium. +// Code quality of this file: low. using System; using System.Collections.Generic; @@ -48,7 +48,16 @@ public static class VariantGenerator { foreach (var @case in variant) { var C = @case.Key; var Ty = @case.Value; - w($" public virtual Immutable.Option<{Ty == null ? "Immutable.Unit" : Ty}> As{C}() => Immutable.Option.None<{Ty == null ? "Immutable.Unit" : Ty}>();"); + // This method is overloaded by the corresponding case's class. + w($" public virtual Immutable.Option<{Ty == null ? "Immutable.Unit" : Ty}> As{C} {{ get => Immutable.Option.None<{Ty == null ? "Immutable.Unit" : Ty}>(); }}"); + } + } + + private static void Is(this Action w, string qualifier, string name, Variant variant) { + foreach (var @case in variant) { + var C = @case.Key; + var Ty = @case.Value; + w($" public virtual bool Is{C} {{ get => As{C}.IsSome; }}"); } } @@ -69,6 +78,19 @@ public static class VariantGenerator { w($" }}"); } + private static void GetValue(this Action w, string qualifier, string name, Variant variant) { + w($" private Immutable.Option GetValue() {{"); + w($" return this.Match("); + w(String.Join(",\n", variant.Select(@case => + $" {@case.Key}: { + @case.Value == null + ? $"() => Immutable.Option.None()" + : $"value => value.Str<{@case.Value}>().Some()" + }"))); + w($" );"); + w($" }}"); + } + private static void Equality(this Action w, string qualifier, string name, Variant variant) { w($" public static bool operator ==({name} a, {name} b)"); w($" => Equality.Operator(a, b);"); @@ -80,6 +102,14 @@ public static class VariantGenerator { w($" public override abstract int GetHashCode();"); } + private static void StringConversion(this Action w, string qualifier, string name, Variant variant) { + w($" public override string ToString()"); + w($" => this.CustomToString();"); + w($" private string CustomToString(params Immutable.Uninstantiatable[] _)"); + w($" => GetTag() + GetValue().Match(some: x => $\"({{x}})\", none: () => \"\");"); + w($" public string Str() => this.ToString();"); + } + private static void CaseValue(this Action w, string qualifier, string name, string C, string Ty) { if (Ty != null) { w($" public readonly {Ty} value;"); @@ -96,7 +126,7 @@ public static class VariantGenerator { } private static void CaseAs(this Action w, string qualifier, string name, string C, string Ty) { - w($" public override Immutable.Option<{Ty == null ? "Immutable.Unit" : Ty}> As{C}() => Immutable.Option.Some<{Ty == null ? "Immutable.Unit" : Ty}>({Ty == null ? "Immutable.Unit.unit" : "this.value"});"); + w($" public override Immutable.Option<{Ty == null ? "Immutable.Unit" : Ty}> As{C} {{ get => Immutable.Option.Some<{Ty == null ? "Immutable.Unit" : Ty}>({Ty == null ? "Immutable.Unit.unit" : "this.value"}); }}"); } private static void CaseEquality(this Action w, string qualifier, string name, string C, string Ty) { @@ -120,10 +150,6 @@ public static class VariantGenerator { } } - private static void CaseToString(this Action w, string qualifier, string name, string C, string Ty) { - w($" public override string ToString() => \"{C}\";"); - } - private static void Cases(this Action w, string qualifier, string name, Variant variant) { foreach (var @case in variant) { var C = @case.Key; @@ -138,8 +164,6 @@ public static class VariantGenerator { w.CaseAs(qualifier, name, C, Ty); w($""); w.CaseEquality(qualifier, name, C, Ty); - w($""); - w.CaseToString(qualifier, name, C, Ty); w($" }}"); } } @@ -148,7 +172,7 @@ public static class VariantGenerator { // Mark as partial to allow defining implicit conversions // and other operators. It would be cleaner to directly // specify these and keep the class impossible to extend. - w($" public abstract partial class {name} : IEquatable<{name}> {{"); + w($" public abstract partial class {name} : IEquatable<{name}>, IString {{"); w.PrivateConstructor(qualifier, name, variant); w($""); w.Visitor(qualifier, name, variant); @@ -159,9 +183,18 @@ public static class VariantGenerator { w($""); w.As(qualifier, name, variant); w($""); + w.Is(qualifier, name, variant); + w($""); w.ChainLens(qualifier, name, variant); w($""); + w.GetTag(qualifier, name, variant); + w($""); + w.GetValue(qualifier, name, variant); + w($""); w.Equality(qualifier, name, variant); + w($""); + w.StringConversion(qualifier, name, variant); + w($""); w($" public static class Cases {{"); w.Cases(qualifier, name, variant); w($" }}"); diff --git a/Utils/Enumerable.cs b/Utils/Enumerable.cs index 0268baa..d119cc3 100644 --- a/Utils/Enumerable.cs +++ b/Utils/Enumerable.cs @@ -120,6 +120,21 @@ public static class Collection { } } + public static Option Last(this IEnumerable ie) { + var e = ie.GetEnumerator(); + T element = default(T); + bool found = false; + while (e.MoveNext()) { + element = e.Current; + found = true; + } + if (found) { + return element.Some(); + } else { + return Option.None(); + } + } + public static Option First(this IEnumerable ie, Func predicate) { var e = ie.GetEnumerator(); while (e.MoveNext()) { @@ -130,6 +145,17 @@ public static class Collection { return Option.None(); } + public static Option First(this IEnumerable ie, Func> selector) { + var e = ie.GetEnumerator(); + while (e.MoveNext()) { + var found = selector(e.Current); + if (found.IsSome) { + return found; + } + } + return Option.None(); + } + public static Option Single(this IEnumerable ie) { var e = ie.GetEnumerator(); if (e.MoveNext()) { @@ -162,7 +188,26 @@ public static class Collection { } } + public static Option Aggregate(this IEnumerable ie, Func f) { + var e = ie.GetEnumerator(); + if (e.MoveNext()) { + var accumulator = e.Current; + while (e.MoveNext()) { + accumulator = f(accumulator, e.Current); + } + return accumulator.Some(); + } + return Option.None(); + } + public static string JoinWith(this IEnumerable strings, string joiner) // TODO: use StringBuilder, there is no complexity info in the docs. => String.Join(joiner, strings); + + public static string JoinToStringWith(this IEnumerable objects, string joiner) + // TODO: use StringBuilder, there is no complexity info in the docs. + => String.Join(joiner, objects.Select(o => o.ToString())); + + public static bool SetEquals(this ImmutableHashSet a, ImmutableHashSet b) + => a.All(x => b.Contains(x)) && b.All(x => a.Contains(x)); } \ No newline at end of file diff --git a/Utils/Global.cs b/Utils/Global.cs index 13bee78..5b0311b 100644 --- a/Utils/Global.cs +++ b/Utils/Global.cs @@ -11,10 +11,13 @@ public static class Global { public static void Log (string str) => Console.WriteLine(str); - public static Unit unit() => Unit.unit; + public static Unit unit { get => Unit.unit; } public static Option None() => Option.None(); public static ImmutableList ImmutableList(params T[] xs) => xs.ToImmutableList(); + + public static ImmutableHashSet ImmutableHashSet(params T[] xs) + => xs.ToImmutableHashSet(); } \ No newline at end of file diff --git a/Utils/Immutable/DefaultDictionary.cs b/Utils/Immutable/DefaultDictionary.cs index 737f119..d28bc40 100644 --- a/Utils/Immutable/DefaultDictionary.cs +++ b/Utils/Immutable/DefaultDictionary.cs @@ -1,9 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Collections.Immutable; -public class ImmutableDefaultDictionary : IEnumerable> { +public class ImmutableDefaultDictionary : IEnumerable>, IString { public readonly TValue defaultValue; public readonly ImmutableDictionary dictionary; @@ -33,6 +34,25 @@ public class ImmutableDefaultDictionary : IEnumerable> GetEnumerator() => dictionary.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => dictionary.GetEnumerator(); + + public ImmutableDefaultDictionaryLens< + TKey, + TValue, + ImmutableDefaultDictionary> + lens { + get => this.ChainLens(x => x); + } + + public override string ToString() + => "ImmutableDefaultDictionary {\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(); } public static class ImmutableDefaultDictionaryExtensionMethods { diff --git a/Utils/Immutable/Dictionary.cs b/Utils/Immutable/Dictionary.cs index fcb4525..9327a50 100644 --- a/Utils/Immutable/Dictionary.cs +++ b/Utils/Immutable/Dictionary.cs @@ -1,3 +1,4 @@ +/* //namespace Immutable { using System; using System.Linq; @@ -8,7 +9,6 @@ using System.Collections.Immutable; // TODO: use Microsoft.FSharp.Collections.FSharpMap - /* public class ImmutableDictionary : Mutable.IReadOnlyDictionary { private readonly Mutable.Dictionary d; private System.Collections.Immutable.ImmutableDictionary i = System.Collections.Immutable.ImmutableDictionary.Empty; diff --git a/Utils/Immutable/Option.cs b/Utils/Immutable/Option.cs index df85c80..847a8da 100644 --- a/Utils/Immutable/Option.cs +++ b/Utils/Immutable/Option.cs @@ -3,6 +3,8 @@ namespace Immutable { public interface Option : System.Collections.Generic.IEnumerable { U Match_(Func some, Func none); + bool IsSome { get; } + bool IsNone { get; } } public static class Option { @@ -17,6 +19,9 @@ namespace Immutable { public U Match_(Func Some, Func None) => Some(value); + public bool IsSome { get => true; } + public bool IsNone { get => false; } + public System.Collections.Generic.IEnumerator GetEnumerator() => value.Singleton().GetEnumerator(); @@ -29,6 +34,9 @@ namespace Immutable { public U Match_(Func Some, Func None) => None(); + public bool IsSome { get => false; } + public bool IsNone { get => true; } + public System.Collections.Generic.IEnumerator GetEnumerator() => System.Linq.Enumerable.Empty().GetEnumerator(); @@ -53,6 +61,9 @@ namespace Immutable { public static Option IfSome(this Option o, Func some) => o.Map(some); + public static Option Bind(this Option o, Func> f) + => o.Match_(some => f(some), () => Option.None()); + public static T Else(this Option o, Func none) => o.Match_(some => some, none); diff --git a/Utils/Immutable/Uninstantiatable.cs b/Utils/Immutable/Uninstantiatable.cs new file mode 100644 index 0000000..f2f1a3f --- /dev/null +++ b/Utils/Immutable/Uninstantiatable.cs @@ -0,0 +1,6 @@ +namespace Immutable { + public sealed class Uninstantiatable { + // This class has no instances. + private Uninstantiatable() {} + } +} \ No newline at end of file diff --git a/Utils/Lens/ImmutableDefaultDictionaryLens.cs b/Utils/Lens/ImmutableDefaultDictionaryLens.cs index 93cf184..346cbfc 100644 --- a/Utils/Lens/ImmutableDefaultDictionaryLens.cs +++ b/Utils/Lens/ImmutableDefaultDictionaryLens.cs @@ -65,18 +65,12 @@ public static class ImmutableDefaultDictionaryLensExtensionMethods { System.Func, Whole> wrap) => new ImmutableDefaultDictionaryLens(wrap: wrap, oldHole: hole); - // TODO: this should be an extension property (once C# supports them) - public static ImmutableDefaultDictionaryLens> - lens( - this ImmutableDefaultDictionary d) - => d.ChainLens(x => x); - // this is a shorthand since we don't have extension properties public static ImmutableDefaultDictionaryValueLens> lens( this ImmutableDefaultDictionary d, TKey key) - => d.lens()[key]; + => d.lens[key]; public static ImmutableDefaultDictionaryValueLens UpdateKey( diff --git a/Utils/OverridableToString.cs b/Utils/OverridableToString.cs new file mode 100644 index 0000000..e69de29 diff --git a/Utils/ToString.cs b/Utils/ToString.cs new file mode 100644 index 0000000..5fcc142 --- /dev/null +++ b/Utils/ToString.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Collections.Immutable; + +public interface IString { + string Str(); +} + +public static class ToStringImplementations { + // These allow string conversion for uncooperative classes + // as long as their type arguments are cooperative. + + // For some reason Str takes precedence over this… + public static string Str(this ImmutableList l) + where T : IString + => $"ImmutableList({l.Select(x => x.Str()).JoinWith(", ")})"; + + // …but not over this: + public static string Str(this ImmutableList l) + => $"ImmutableList({l.Select(x => x.Str()).JoinWith(", ")})"; + + public static string Str(this ImmutableList l) + => $"ImmutableList({l.Select(x => x.Str()).JoinWith(", ")})"; + + public static string Str(this ImmutableHashSet h) + => $"ImmutableHashSet({h.Select(x => x.Str()).JoinWith(", ")})"; + + public static string Str(this string s) => $"\"{s}\""; + + public static string Str(this object o) => ""+o.ToString(); +} \ No newline at end of file diff --git a/main.cs b/main.cs index 494f8cd..d2829d2 100644 --- a/main.cs +++ b/main.cs @@ -18,7 +18,7 @@ public static class MainClass { .Pipe(c => dest.Write(c)); } - public static void RunTest(string toolchainName, Compiler compile, Exe runner, File source) { + public static bool RunTest(string toolchainName, Compiler compile, Exe runner, File source) { var destPath = tests_results.Combine(source); var sourcePath = tests.Combine(source); var expected = sourcePath.DropExtension().Combine(Ext(".o")); @@ -27,15 +27,29 @@ public static class MainClass { destPath.DirName().Create(); - CompileToFile(compile, sourcePath, destPath); - - var actualStr = runner.Run(destPath); - var expectedStr = expected.Read(); - if (actualStr != expectedStr) { + UserErrorException exception = null; + try { + CompileToFile(compile, sourcePath, destPath); + } catch (UserErrorException e) { + exception = e; + } + + if (exception != null) { + Console.WriteLine(""); Console.WriteLine("\x1b[1;31mFail\x1b[m"); - throw new TestFailedException($"{source}: expected {expectedStr} but got {actualStr}."); + Console.WriteLine($"\x1b[1;33m{exception.Message}\x1b[m\n"); + return false; } else { - Console.Write("\x1b[1;32mOK\x1b[m\r"); + var actualStr = runner.Run(destPath); + var expectedStr = expected.Read(); + if (actualStr != expectedStr) { + Console.WriteLine("\x1b[1;31mFail\x1b[m"); + Console.WriteLine($"\x1b[1;33m{source}: expected {expectedStr} but got {actualStr}.\x1b[m\n"); + return false; + } else { + Console.Write("\x1b[1;32mOK\x1b[m\r"); + return true; + } } } @@ -48,13 +62,22 @@ public static class MainClass { .Cons("eval", Evaluator.Evaluate, Exe("cat")); var total = 0; + var passed = 0; + var failed = 0; foreach (var t in Dir("Tests/").GetFiles("*.e", SearchOption.AllDirectories)) { foreach (var compiler in compilers) { - RunTest(compiler.Item1, compiler.Item2, compiler.Item3, t); + if (RunTest(compiler.Item1, compiler.Item2, compiler.Item3, t)) { + passed++; + } else { + failed++; + } total++; } } - Console.WriteLine($"\x1b[K{total} tests run."); + Console.WriteLine($"\x1b[K{passed}/{total} tests passed, {failed} failed."); + if (failed != 0) { + Environment.Exit(1); + } } public static void Main (string[] args) {