diff --git a/src/Draco.Compiler.Tests/EndToEnd/CompilingCodeTests.cs b/src/Draco.Compiler.Tests/EndToEnd/CompilingCodeTests.cs index ab3ec1e9d..e458095fe 100644 --- a/src/Draco.Compiler.Tests/EndToEnd/CompilingCodeTests.cs +++ b/src/Draco.Compiler.Tests/EndToEnd/CompilingCodeTests.cs @@ -683,4 +683,26 @@ public void SingleInterpolatedElement() var x = Invoke(assembly, "stringify", 123); Assert.Equal("123", x); } + + [Fact] + public void BasicMatchOnNumbers() + { + var assembly = Compile("""" + public func categorize(a: int32): string = match (a) { + 1 -> "one"; + 2 -> "two"; + _ if (a rem 2 == 0) -> "even"; + _ -> "odd"; + }; + """"); + + var input = new[] { 1, 2, 3, 4, 7, 8 }; + var output = new[] { "one", "two", "odd", "even", "odd", "even" }; + + foreach (var (inp, expectedOut) in input.Zip(output)) + { + var got = Invoke(assembly, "categorize", inp); + Assert.Equal(expectedOut, got); + } + } } diff --git a/src/Draco.Compiler.Tests/EndToEnd/EndToEndTestsBase.cs b/src/Draco.Compiler.Tests/EndToEnd/EndToEndTestsBase.cs index e0031af55..d90f56598 100644 --- a/src/Draco.Compiler.Tests/EndToEnd/EndToEndTestsBase.cs +++ b/src/Draco.Compiler.Tests/EndToEnd/EndToEndTestsBase.cs @@ -80,7 +80,7 @@ protected static TResult Invoke( string methodName, TextReader? stdin = null, TextWriter? stdout = null, - string moduleName = CompilerConstants.DefaultModuleName, + string moduleName = Constants.DefaultModuleName, params object[] args) { Console.SetIn(stdin ?? Console.In); @@ -106,7 +106,7 @@ protected static TResult Invoke(Assembly assembly, string moduleName, s protected static TResult Invoke(Assembly assembly, string methodName, params object[] args) => Invoke( assembly: assembly, - moduleName: CompilerConstants.DefaultModuleName, + moduleName: Constants.DefaultModuleName, methodName: methodName, stdin: null, stdout: null, diff --git a/src/Draco.Compiler.Tests/Semantics/FlowAnalysisTests.cs b/src/Draco.Compiler.Tests/Semantics/FlowAnalysisTests.cs index b98ad3d94..c5506ddd2 100644 --- a/src/Draco.Compiler.Tests/Semantics/FlowAnalysisTests.cs +++ b/src/Draco.Compiler.Tests/Semantics/FlowAnalysisTests.cs @@ -548,7 +548,7 @@ public void LocalValReassigned() public void GlobalValReassigned() { // val x: int32 = 0; - // func foo() { + // func foo() { // x = 1; // } @@ -571,4 +571,143 @@ public void GlobalValReassigned() Assert.Single(diags); AssertDiagnostic(diags, FlowAnalysisErrors.ImmutableVariableCanNotBeAssignedTo); } + + [Fact] + public void ExhaustiveMatch() + { + // func dirac_pulse(n: int32): int32 = match (n) { + // 0 -> 1; + // _ -> 0; + // } + + // Arrange + var tree = SyntaxTree.Create(CompilationUnit(FunctionDeclaration( + "dirac_pulse", + ParameterList(Parameter("n", NameType("int32"))), + NameType("int32"), + InlineFunctionBody(MatchExpression(NameExpression("n"), + MatchArm(LiteralPattern(0), LiteralExpression(1)), + MatchArm(DiscardPattern(), LiteralExpression(0))))))); + + // Act + var compilation = CreateCompilation(tree); + var semanticModel = compilation.GetSemanticModel(tree); + var diags = semanticModel.Diagnostics; + + // Assert + Assert.Empty(diags); + } + + [Fact] + public void NonExhaustiveMatch() + { + // func dirac_pulse(n: int32): int32 = match (n) { + // 0 -> 1; + // 1 -> 0; + // } + + // Arrange + var tree = SyntaxTree.Create(CompilationUnit(FunctionDeclaration( + "dirac_pulse", + ParameterList(Parameter("n", NameType("int32"))), + NameType("int32"), + InlineFunctionBody(MatchExpression(NameExpression("n"), + MatchArm(LiteralPattern(0), LiteralExpression(1)), + MatchArm(LiteralPattern(1), LiteralExpression(0))))))); + + // Act + var compilation = CreateCompilation(tree); + var semanticModel = compilation.GetSemanticModel(tree); + var diags = semanticModel.Diagnostics; + + // Assert + Assert.Single(diags); + AssertDiagnostic(diags, FlowAnalysisErrors.NonExhaustiveMatchExpression); + } + + [Fact] + public void RedundantMatchArm() + { + // func dirac_pulse(n: int32): int32 = match (n) { + // 0 -> 1; + // 0 -> 0; + // _ -> 1; + // } + + // Arrange + var tree = SyntaxTree.Create(CompilationUnit(FunctionDeclaration( + "dirac_pulse", + ParameterList(Parameter("n", NameType("int32"))), + NameType("int32"), + InlineFunctionBody(MatchExpression(NameExpression("n"), + MatchArm(LiteralPattern(0), LiteralExpression(1)), + MatchArm(LiteralPattern(0), LiteralExpression(0)), + MatchArm(DiscardPattern(), LiteralExpression(1))))))); + + // Act + var compilation = CreateCompilation(tree); + var semanticModel = compilation.GetSemanticModel(tree); + var diags = semanticModel.Diagnostics; + + // Assert + Assert.Single(diags); + AssertDiagnostic(diags, FlowAnalysisErrors.MatchPatternAlreadyHandled); + } + + [Fact] + public void RedundantMatchArmWithCondition() + { + // func dirac_pulse(n: int32): int32 = match (n) { + // 0 -> 1; + // _ if (false) -> 0; + // _ -> 1; + // } + + // Arrange + var tree = SyntaxTree.Create(CompilationUnit(FunctionDeclaration( + "dirac_pulse", + ParameterList(Parameter("n", NameType("int32"))), + NameType("int32"), + InlineFunctionBody(MatchExpression(NameExpression("n"), + MatchArm(LiteralPattern(0), LiteralExpression(1)), + MatchArm(DiscardPattern(), LiteralExpression(false), LiteralExpression(0)), + MatchArm(DiscardPattern(), LiteralExpression(1))))))); + + // Act + var compilation = CreateCompilation(tree); + var semanticModel = compilation.GetSemanticModel(tree); + var diags = semanticModel.Diagnostics; + + // Assert + Assert.Empty(diags); + } + + [Fact] + public void RedundantMatchArmWithConditionLast() + { + // func dirac_pulse(n: int32): int32 = match (n) { + // 0 -> 1; + // _ -> 0; + // _ if (false) -> 1; + // } + + // Arrange + var tree = SyntaxTree.Create(CompilationUnit(FunctionDeclaration( + "dirac_pulse", + ParameterList(Parameter("n", NameType("int32"))), + NameType("int32"), + InlineFunctionBody(MatchExpression(NameExpression("n"), + MatchArm(LiteralPattern(0), LiteralExpression(1)), + MatchArm(DiscardPattern(), LiteralExpression(0)), + MatchArm(DiscardPattern(), LiteralExpression(false), LiteralExpression(1))))))); + + // Act + var compilation = CreateCompilation(tree); + var semanticModel = compilation.GetSemanticModel(tree); + var diags = semanticModel.Diagnostics; + + // Assert + Assert.Single(diags); + AssertDiagnostic(diags, FlowAnalysisErrors.MatchPatternAlreadyHandled); + } } diff --git a/src/Draco.Compiler.Tests/Semantics/IntegralDomainTests.cs b/src/Draco.Compiler.Tests/Semantics/IntegralDomainTests.cs new file mode 100644 index 000000000..b16659111 --- /dev/null +++ b/src/Draco.Compiler.Tests/Semantics/IntegralDomainTests.cs @@ -0,0 +1,109 @@ +using Draco.Compiler.Internal.FlowAnalysis.Domain; +using static Draco.Compiler.Internal.BoundTree.BoundTreeFactory; + +namespace Draco.Compiler.Tests.Semantics; + +public sealed class IntegralDomainTests +{ + private static IntegralDomain Interval(int min, int max) => new(null!, min, max); + + [Fact] + public void RemovingFullDomain() + { + var d = Interval(-10, 10); + + d.SubtractPattern(DiscardPattern()); + + Assert.True(d.IsEmpty); + Assert.Null(d.SampleValue()); + } + + [Fact] + public void RemovingZero() + { + var d = Interval(-10, 10); + + d.SubtractPattern(LiteralPattern(0, null!)); + + Assert.False(d.IsEmpty); + Assert.Equal(1, d.SampleValue()); + } + + [Fact] + public void RemovingAllPositive() + { + var d = Interval(-1, 1); + + d.SubtractPattern(LiteralPattern(1, null!)); + + Assert.False(d.IsEmpty); + Assert.Equal(0, d.SampleValue()); + } + + [Fact] + public void RemovingAllNonNegative() + { + var d = Interval(-1, 1); + + d.SubtractPattern(LiteralPattern(0, null!)); + d.SubtractPattern(LiteralPattern(1, null!)); + + Assert.False(d.IsEmpty); + Assert.Equal(-1, d.SampleValue()); + } + + [Fact] + public void RemovingAll() + { + var d = Interval(-1, 1); + + d.SubtractPattern(LiteralPattern(-1, null!)); + d.SubtractPattern(LiteralPattern(0, null!)); + d.SubtractPattern(LiteralPattern(1, null!)); + + Assert.True(d.IsEmpty); + Assert.Null(d.SampleValue()); + } + + [Fact] + public void RemovingAllButZero() + { + var d = Interval(-1, 1); + + d.SubtractPattern(LiteralPattern(1, null!)); + d.SubtractPattern(LiteralPattern(-1, null!)); + + Assert.False(d.IsEmpty); + Assert.Equal(0, d.SampleValue()); + } + + [Fact] + public void RemovingNeighbors() + { + var d = Interval(-10, 10); + + d.SubtractPattern(LiteralPattern(1, null!)); + d.SubtractPattern(LiteralPattern(-1, null!)); + d.SubtractPattern(LiteralPattern(0, null!)); + + Assert.Equal("[-10; -1) U (1; 10]", d.ToString()); + } + + [Fact] + public void RemovingBetweenRemovedIntervals() + { + var d = Interval(0, 10); + + d.SubtractPattern(LiteralPattern(1, null!)); + d.SubtractPattern(LiteralPattern(2, null!)); + + d.SubtractPattern(LiteralPattern(4, null!)); + d.SubtractPattern(LiteralPattern(5, null!)); + + Assert.Equal("[0; 1) U (2; 4) U (5; 10]", d.ToString()); + + d.SubtractPattern(LiteralPattern(3, null!)); + + Assert.Equal("[0; 1) U (5; 10]", d.ToString()); + } +} diff --git a/src/Draco.Compiler/Api/Compilation.cs b/src/Draco.Compiler/Api/Compilation.cs index de35c071f..388c9ba4f 100644 --- a/src/Draco.Compiler/Api/Compilation.cs +++ b/src/Draco.Compiler/Api/Compilation.cs @@ -16,6 +16,7 @@ using Draco.Compiler.Internal.Symbols.Metadata; using Draco.Compiler.Internal.Symbols.Source; using Draco.Compiler.Internal.Symbols.Synthetized; +using Draco.Compiler.Internal.Utilities; using ModuleSymbol = Draco.Compiler.Internal.Symbols.ModuleSymbol; namespace Draco.Compiler.Api; diff --git a/src/Draco.Compiler/Api/Diagnostics/Diagnostic_Builder.cs b/src/Draco.Compiler/Api/Diagnostics/Diagnostic_Builder.cs index 4166f0217..3b53d6107 100644 --- a/src/Draco.Compiler/Api/Diagnostics/Diagnostic_Builder.cs +++ b/src/Draco.Compiler/Api/Diagnostics/Diagnostic_Builder.cs @@ -69,7 +69,7 @@ public Builder WithFormatArgs(params object?[] args) return this; } - public Builder WithLocation(Location location) + public Builder WithLocation(Location? location) { this.Location = location; return this; diff --git a/src/Draco.Compiler/Api/Semantics/SemanticModel.cs b/src/Draco.Compiler/Api/Semantics/SemanticModel.cs index 0da233078..2188b43c0 100644 --- a/src/Draco.Compiler/Api/Semantics/SemanticModel.cs +++ b/src/Draco.Compiler/Api/Semantics/SemanticModel.cs @@ -6,12 +6,12 @@ using System.Linq; using Draco.Compiler.Api.Diagnostics; using Draco.Compiler.Api.Syntax; -using Draco.Compiler.Internal; using Draco.Compiler.Internal.Binding; using Draco.Compiler.Internal.BoundTree; using Draco.Compiler.Internal.Diagnostics; using Draco.Compiler.Internal.Symbols; using Draco.Compiler.Internal.Symbols.Source; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Api.Semantics; diff --git a/src/Draco.Compiler/Api/Syntax/SeparatedSyntaxList.cs b/src/Draco.Compiler/Api/Syntax/SeparatedSyntaxList.cs index e83def4a9..1492a49bc 100644 --- a/src/Draco.Compiler/Api/Syntax/SeparatedSyntaxList.cs +++ b/src/Draco.Compiler/Api/Syntax/SeparatedSyntaxList.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Draco.Compiler.Internal; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Api.Syntax; diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs b/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs index 0f74a4d34..c7eadf028 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs @@ -280,6 +280,33 @@ public static ForExpressionSyntax ForExpression( sequence, body); + public static MatchExpressionSyntax MatchExpression( + ExpressionSyntax matchedValue, + IEnumerable matchArms) => MatchExpression( + Match, + OpenParen, + matchedValue, + CloseParen, + OpenBrace, + SyntaxList(matchArms), + CloseBrace); + public static MatchExpressionSyntax MatchExpression( + ExpressionSyntax matchedValue, + params MatchArmSyntax[] matchArms) => MatchExpression(matchedValue, matchArms.AsEnumerable()); + + public static MatchArmSyntax MatchArm( + PatternSyntax pattern, + ExpressionSyntax? guard, + ExpressionSyntax value) => MatchArm( + pattern, + guard is null ? null : GuardClause(If, OpenParen, guard, CloseParen), + Arrow, + value, + Semicolon); + public static MatchArmSyntax MatchArm( + PatternSyntax pattern, + ExpressionSyntax value) => MatchArm(pattern, null, value); + public static CallExpressionSyntax CallExpression( ExpressionSyntax called, IEnumerable args) => CallExpression( @@ -329,6 +356,9 @@ public static StringExpressionSyntax StringExpression(string value) => public static TextStringPartSyntax TextStringPart(string value) => TextStringPart(MakeToken(TokenKind.StringContent, value, value)); + public static LiteralPatternSyntax LiteralPattern(int value) => LiteralPattern(Integer(value)); + public static DiscardPatternSyntax DiscardPattern() => DiscardPattern(Discard); + // TOKENS ////////////////////////////////////////////////////////////////// public static SyntaxToken EndOfInput { get; } = MakeToken(TokenKind.EndOfInput); @@ -349,6 +379,8 @@ public static TextStringPartSyntax TextStringPart(string value) => public static SyntaxToken Func { get; } = MakeToken(TokenKind.KeywordFunc); public static SyntaxToken Goto { get; } = MakeToken(TokenKind.KeywordGoto); public static SyntaxToken Module { get; } = MakeToken(TokenKind.KeywordModule); + public static SyntaxToken Match { get; } = MakeToken(TokenKind.KeywordMatch); + public static SyntaxToken Discard { get; } = MakeToken(TokenKind.KeywordDiscard); public static SyntaxToken True { get; } = MakeToken(TokenKind.KeywordTrue, true); public static SyntaxToken False { get; } = MakeToken(TokenKind.KeywordFalse, false); public static SyntaxToken OpenBrace { get; } = MakeToken(TokenKind.CurlyOpen); @@ -361,6 +393,7 @@ public static TextStringPartSyntax TextStringPart(string value) => public static SyntaxToken PlusAssign { get; } = MakeToken(TokenKind.PlusAssign); public static SyntaxToken LessThan { get; } = MakeToken(TokenKind.LessThan); public static SyntaxToken GreaterThan { get; } = MakeToken(TokenKind.GreaterThan); + public static SyntaxToken Arrow { get; } = MakeToken(TokenKind.Arrow); public static SyntaxToken LineStringStart { get; } = MakeToken(TokenKind.LineStringStart, "\""); public static SyntaxToken LineStringEnd { get; } = MakeToken(TokenKind.LineStringEnd, "\""); public static SyntaxToken Ellipsis { get; } = MakeToken(TokenKind.Ellipsis); diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs b/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs index d51a7bde5..5d94a1b72 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs @@ -18,6 +18,7 @@ public static class SyntaxFacts TokenKind.EndOfInput => string.Empty, TokenKind.InterpolationEnd => "}", TokenKind.KeywordAnd => "and", + TokenKind.KeywordDiscard => "_", TokenKind.KeywordElse => "else", TokenKind.KeywordFalse => "false", TokenKind.KeywordFor => "for", @@ -27,6 +28,7 @@ public static class SyntaxFacts TokenKind.KeywordImport => "import", TokenKind.KeywordIn => "in", TokenKind.KeywordInternal => "internal", + TokenKind.KeywordMatch => "match", TokenKind.KeywordMod => "mod", TokenKind.KeywordModule => "module", TokenKind.KeywordNot => "not", @@ -64,6 +66,7 @@ public static class SyntaxFacts TokenKind.StarAssign => "*=", TokenKind.SlashAssign => "/=", TokenKind.Ellipsis => "...", + TokenKind.Arrow => "->", _ => null, }; diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxList.cs b/src/Draco.Compiler/Api/Syntax/SyntaxList.cs index 896f9fc92..2dc581459 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxList.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxList.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Draco.Compiler.Internal; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Api.Syntax; diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxNode.cs b/src/Draco.Compiler/Api/Syntax/SyntaxNode.cs index a329b6524..ee4618faf 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxNode.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxNode.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Draco.Compiler.Api.Diagnostics; -using Draco.Compiler.Internal; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Api.Syntax; diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs b/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs index 8bafdfa6e..a2e63676e 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs @@ -3,10 +3,10 @@ using System.Collections.Immutable; using System.Linq; using Draco.Compiler.Api.Diagnostics; -using Draco.Compiler.Internal; using Draco.Compiler.Internal.Syntax; using Draco.Compiler.Internal.Syntax.Formatting; using Draco.Compiler.Internal.Syntax.Rewriting; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Api.Syntax; diff --git a/src/Draco.Compiler/Api/Syntax/TokenKind.cs b/src/Draco.Compiler/Api/Syntax/TokenKind.cs index 9c6c533e5..8e48af132 100644 --- a/src/Draco.Compiler/Api/Syntax/TokenKind.cs +++ b/src/Draco.Compiler/Api/Syntax/TokenKind.cs @@ -80,6 +80,11 @@ public enum TokenKind /// KeywordAnd, + /// + /// The keyword '_'. + /// + KeywordDiscard, + /// /// The keyword 'else'. /// @@ -125,6 +130,11 @@ public enum TokenKind /// KeywordInternal, + /// + /// The keyword 'match'. + /// + KeywordMatch, + /// /// The keyword 'mod'. /// @@ -309,4 +319,9 @@ public enum TokenKind /// '...'. /// Ellipsis, + + /// + /// '->'. + /// + Arrow, } diff --git a/src/Draco.Compiler/Internal/Binding/Binder_BoundExpression.cs b/src/Draco.Compiler/Internal/Binding/Binder_BoundExpression.cs index 9a4b5de89..aceb1a091 100644 --- a/src/Draco.Compiler/Internal/Binding/Binder_BoundExpression.cs +++ b/src/Draco.Compiler/Internal/Binding/Binder_BoundExpression.cs @@ -52,6 +52,7 @@ internal partial class Binder UntypedAndExpression and => this.TypeAndExpression(and, constraints, diagnostics), UntypedOrExpression or => this.TypeOrExpression(or, constraints, diagnostics), UntypedMemberExpression mem => this.TypeMemberExpression(mem, constraints, diagnostics), + UntypedMatchExpression match => this.TypeMatchExpression(match, constraints, diagnostics), UntypedDelayedExpression delay => this.TypeDelayedExpression(delay, constraints, diagnostics), _ => throw new ArgumentOutOfRangeException(nameof(expression)), }; @@ -402,6 +403,25 @@ private BoundExpression TypeMemberExpression(UntypedMemberExpression mem, Constr } } + private BoundExpression TypeMatchExpression(UntypedMatchExpression match, ConstraintSolver constraints, DiagnosticBag diagnostics) + { + var matchedValue = this.TypeExpression(match.MatchedValue, constraints, diagnostics); + + var arms = ImmutableArray.CreateBuilder(); + foreach (var arm in match.MatchArms) + { + var pattern = this.TypePattern(arm.Pattern, constraints, diagnostics); + var guard = arm.Guard is null + ? null + : this.TypeExpression(arm.Guard, constraints, diagnostics); + var value = this.TypeExpression(arm.Value, constraints, diagnostics); + + arms.Add(new(arm.Syntax, pattern, guard, value)); + } + + return new BoundMatchExpression(match.Syntax, matchedValue, arms.ToImmutable(), match.TypeRequired); + } + private BoundExpression TypeDelayedExpression(UntypedDelayedExpression delay, ConstraintSolver constraints, DiagnosticBag diagnostics) { // Just take result and type that diff --git a/src/Draco.Compiler/Internal/Binding/Binder_BoundPattern.cs b/src/Draco.Compiler/Internal/Binding/Binder_BoundPattern.cs new file mode 100644 index 000000000..56516e4ab --- /dev/null +++ b/src/Draco.Compiler/Internal/Binding/Binder_BoundPattern.cs @@ -0,0 +1,31 @@ +using System; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.Diagnostics; +using Draco.Compiler.Internal.Solver; +using Draco.Compiler.Internal.UntypedTree; + +namespace Draco.Compiler.Internal.Binding; + +internal partial class Binder +{ + /// + /// Binds the given untyped pattern to a bound pattern. + /// + /// The untyped pattern to bind. + /// The constraints that has been collected during the binding process. + /// The diagnostics produced during the process. + /// The bound expression for . + internal virtual BoundPattern TypePattern(UntypedPattern pattern, ConstraintSolver constraints, DiagnosticBag diagnostics) => pattern switch + { + UntypedUnexpectedPattern => new BoundUnexpectedPattern(pattern.Syntax), + UntypedDiscardPattern discard => this.TypeDiscardPattern(discard, constraints, diagnostics), + UntypedLiteralPattern literal => this.TypeLiteralPattern(literal, constraints, diagnostics), + _ => throw new ArgumentOutOfRangeException(nameof(pattern)), + }; + + private BoundPattern TypeDiscardPattern(UntypedDiscardPattern discard, ConstraintSolver constraints, DiagnosticBag diagnostics) => + new BoundDiscardPattern(discard.Syntax); + + private BoundPattern TypeLiteralPattern(UntypedLiteralPattern literal, ConstraintSolver constraints, DiagnosticBag diagnostics) => + new BoundLiteralPattern(literal.Syntax, literal.Value, literal.Type); +} diff --git a/src/Draco.Compiler/Internal/Binding/Binder_UntypedExpression.cs b/src/Draco.Compiler/Internal/Binding/Binder_UntypedExpression.cs index 9c3f6a703..6cdb4200e 100644 --- a/src/Draco.Compiler/Internal/Binding/Binder_UntypedExpression.cs +++ b/src/Draco.Compiler/Internal/Binding/Binder_UntypedExpression.cs @@ -45,6 +45,7 @@ internal partial class Binder MemberExpressionSyntax maccess => this.BindMemberExpression(maccess, constraints, diagnostics), GenericExpressionSyntax gen => this.BindGenericExpression(gen, constraints, diagnostics), IndexExpressionSyntax index => this.BindIndexExpression(index, constraints, diagnostics), + MatchExpressionSyntax match => this.BindMatchExpression(match, constraints, diagnostics), _ => throw new ArgumentOutOfRangeException(nameof(syntax)), }; @@ -720,6 +721,33 @@ private UntypedExpression BindGenericExpression(GenericExpressionSyntax syntax, } } + private UntypedExpression BindMatchExpression(MatchExpressionSyntax syntax, ConstraintSolver constraints, DiagnosticBag diagnostics) + { + var matchedValue = this.BindExpression(syntax.MatchedValue, constraints, diagnostics); + + var arms = ImmutableArray.CreateBuilder(); + var armValueTypes = ImmutableArray.CreateBuilder(); + + foreach (var armSyntax in syntax.MatchArms) + { + var pattern = this.BindPattern(armSyntax.Pattern, constraints, diagnostics); + constraints.Assignable(pattern.Type, matchedValue.TypeRequired, armSyntax.Pattern); + + var guard = armSyntax.GuardClause is null + ? null + : this.BindExpression(armSyntax.GuardClause.Condition, constraints, diagnostics); + var value = this.BindExpression(armSyntax.Value, constraints, diagnostics); + armValueTypes.Add(value.TypeRequired); + + arms.Add(new(armSyntax, pattern, guard, value)); + } + + var commonType = constraints.AllocateTypeVariable(); + constraints.CommonType(commonType, armValueTypes.ToImmutable(), syntax); + + return new UntypedMatchExpression(syntax, matchedValue, arms.ToImmutable(), commonType); + } + private UntypedExpression SymbolToExpression(SyntaxNode syntax, Symbol symbol, ConstraintSolver constraints, DiagnosticBag diagnostics) { if (symbol.IsError) return new UntypedReferenceErrorExpression(syntax, symbol); diff --git a/src/Draco.Compiler/Internal/Binding/Binder_UntypedPattern.cs b/src/Draco.Compiler/Internal/Binding/Binder_UntypedPattern.cs new file mode 100644 index 000000000..0bb70f9e2 --- /dev/null +++ b/src/Draco.Compiler/Internal/Binding/Binder_UntypedPattern.cs @@ -0,0 +1,37 @@ +using System; +using Draco.Compiler.Api.Syntax; +using Draco.Compiler.Internal.Diagnostics; +using Draco.Compiler.Internal.Solver; +using Draco.Compiler.Internal.UntypedTree; + +namespace Draco.Compiler.Internal.Binding; + +internal partial class Binder +{ + /// + /// Binds the given syntax node to an untyped pattern. + /// + /// The syntax to bind. + /// The constraints that has been collected during the binding process. + /// The diagnostics produced during the process. + /// The untyped pattern for . + protected virtual UntypedPattern BindPattern(SyntaxNode syntax, ConstraintSolver constraints, DiagnosticBag diagnostics) => syntax switch + { + UnexpectedPatternSyntax => new UntypedUnexpectedPattern(syntax), + DiscardPatternSyntax discard => this.BindDiscardPattern(discard, constraints, diagnostics), + LiteralPatternSyntax literal => this.BindLiteralPattern(literal, constraints, diagnostics), + _ => throw new ArgumentOutOfRangeException(nameof(syntax)), + }; + + private UntypedPattern BindDiscardPattern(DiscardPatternSyntax syntax, ConstraintSolver constraints, DiagnosticBag diagnostics) => + new UntypedDiscardPattern(syntax); + + private UntypedPattern BindLiteralPattern(LiteralPatternSyntax syntax, ConstraintSolver constraints, DiagnosticBag diagnostics) + { + if (!BinderFacts.TryGetLiteralType(syntax.Literal.Value, this.IntrinsicSymbols, out var literalType)) + { + throw new InvalidOperationException("can not determine literal type"); + } + return new UntypedLiteralPattern(syntax, syntax.Literal.Value, literalType); + } +} diff --git a/src/Draco.Compiler/Internal/Binding/ImportBinder.cs b/src/Draco.Compiler/Internal/Binding/ImportBinder.cs index 3943881f4..a4951a055 100644 --- a/src/Draco.Compiler/Internal/Binding/ImportBinder.cs +++ b/src/Draco.Compiler/Internal/Binding/ImportBinder.cs @@ -8,6 +8,7 @@ using Draco.Compiler.Internal.Diagnostics; using Draco.Compiler.Internal.Symbols; using Draco.Compiler.Internal.Symbols.Error; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Binding; diff --git a/src/Draco.Compiler/Internal/BoundTree/BoundNode.cs b/src/Draco.Compiler/Internal/BoundTree/BoundNode.cs index ce0ed715d..4fec6c826 100644 --- a/src/Draco.Compiler/Internal/BoundTree/BoundNode.cs +++ b/src/Draco.Compiler/Internal/BoundTree/BoundNode.cs @@ -197,3 +197,21 @@ internal partial class BoundArrayAccessLvalue { public override TypeSymbol Type => this.Array.TypeRequired.GenericArguments[0]; } + +// Patterns + +internal partial class BoundPattern +{ + public abstract TypeSymbol Type { get; } +} + +internal partial class BoundUnexpectedPattern +{ + public override TypeSymbol Type => IntrinsicSymbols.Never; +} + +internal partial class BoundDiscardPattern +{ + public static BoundDiscardPattern Default { get; } = new(null); + public override TypeSymbol Type => IntrinsicSymbols.Never; +} diff --git a/src/Draco.Compiler/Internal/BoundTree/BoundNodes.xml b/src/Draco.Compiler/Internal/BoundTree/BoundNodes.xml index 825831fd2..36bc22a25 100644 --- a/src/Draco.Compiler/Internal/BoundTree/BoundNodes.xml +++ b/src/Draco.Compiler/Internal/BoundTree/BoundNodes.xml @@ -9,6 +9,7 @@ + @@ -92,6 +93,18 @@ + + + + + + + + + + + + @@ -288,4 +301,17 @@ + + + + + + + + + + + + + diff --git a/src/Draco.Compiler/Internal/Codegen/CilCodegen.cs b/src/Draco.Compiler/Internal/Codegen/CilCodegen.cs index 6b3647482..9223fcd72 100644 --- a/src/Draco.Compiler/Internal/Codegen/CilCodegen.cs +++ b/src/Draco.Compiler/Internal/Codegen/CilCodegen.cs @@ -455,6 +455,9 @@ private void EncodePush(IOperand operand) case Constant c: switch (c.Value) { + case null: + this.InstructionEncoder.OpCode(ILOpCode.Ldnull); + break; case int i: this.InstructionEncoder.LoadConstantI4(i); break; diff --git a/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs b/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs index a0e05277a..5a75212eb 100644 --- a/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs +++ b/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs @@ -124,7 +124,7 @@ public TypeReferenceHandle GetModuleReferenceHandle(IModule module) // We take its parent module : this.GetModuleReferenceHandle(module.Parent); var name = string.IsNullOrEmpty(module.Name) - ? CompilerConstants.DefaultModuleName + ? Constants.DefaultModuleName : module.Name; handle = this.GetOrAddTypeReference( parent: resolutionScope, @@ -420,7 +420,7 @@ private void EncodeModule(IModule module, TypeDefinitionHandle? parentModule = n } var attributes = visibility | TypeAttributes.Class | TypeAttributes.AutoLayout | TypeAttributes.BeforeFieldInit | TypeAttributes.Abstract | TypeAttributes.Sealed; - var name = string.IsNullOrEmpty(module.Name) ? CompilerConstants.DefaultModuleName : module.Name; + var name = string.IsNullOrEmpty(module.Name) ? Constants.DefaultModuleName : module.Name; // Create the type var createdModule = this.AddTypeDefinition( diff --git a/src/Draco.Compiler/Internal/CompilerConstants.cs b/src/Draco.Compiler/Internal/Constants.cs similarity index 89% rename from src/Draco.Compiler/Internal/CompilerConstants.cs rename to src/Draco.Compiler/Internal/Constants.cs index f265184b3..28c7667f0 100644 --- a/src/Draco.Compiler/Internal/CompilerConstants.cs +++ b/src/Draco.Compiler/Internal/Constants.cs @@ -1,6 +1,6 @@ namespace Draco.Compiler.Internal; -internal static class CompilerConstants +internal static class Constants { /// /// The default name of the root module in IL. diff --git a/src/Draco.Compiler/Internal/Declarations/MergedModuleDeclaration.cs b/src/Draco.Compiler/Internal/Declarations/MergedModuleDeclaration.cs index 71dacaa52..a3d269dd6 100644 --- a/src/Draco.Compiler/Internal/Declarations/MergedModuleDeclaration.cs +++ b/src/Draco.Compiler/Internal/Declarations/MergedModuleDeclaration.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Linq; using Draco.Compiler.Api.Syntax; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Declarations; diff --git a/src/Draco.Compiler/Internal/Declarations/SingleModuleDeclaration.cs b/src/Draco.Compiler/Internal/Declarations/SingleModuleDeclaration.cs index 57bc68146..7fdcca238 100644 --- a/src/Draco.Compiler/Internal/Declarations/SingleModuleDeclaration.cs +++ b/src/Draco.Compiler/Internal/Declarations/SingleModuleDeclaration.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Linq; using Draco.Compiler.Api.Syntax; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Declarations; diff --git a/src/Draco.Compiler/Internal/Documentation/SymbolDocumentation.cs b/src/Draco.Compiler/Internal/Documentation/SymbolDocumentation.cs index 89b619226..10678c1c4 100644 --- a/src/Draco.Compiler/Internal/Documentation/SymbolDocumentation.cs +++ b/src/Draco.Compiler/Internal/Documentation/SymbolDocumentation.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Documentation; diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/DecisionTree.cs b/src/Draco.Compiler/Internal/FlowAnalysis/DecisionTree.cs new file mode 100644 index 000000000..3728a5e40 --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/DecisionTree.cs @@ -0,0 +1,590 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.FlowAnalysis.Domain; +using Draco.Compiler.Internal.Symbols.Synthetized; +using Draco.Compiler.Internal.Utilities; + +namespace Draco.Compiler.Internal.FlowAnalysis; + +/// +/// Utilities for building a decision tree. +/// +internal static class DecisionTree +{ + /// + /// Builds a decision tree from the given data. + /// + /// The action type. + /// The intrinsic symbols of the compilation. + /// The matched value. + /// The arms of the match. + /// The constructed decision tree. + public static DecisionTree Build( + IntrinsicSymbols intrinsicSymbols, + BoundExpression matchedValue, + ImmutableArray.Arm> arms) + where TAction : class => + DecisionTree.Build(intrinsicSymbols, matchedValue, arms); + + /// + /// Utility for constructing an arm for a decision tree. + /// + /// The action type. + /// The arm pattern. + /// The guard expression, if any. + /// The associated action. + /// The constructed decision tree arm. + public static DecisionTree.Arm Arm(BoundPattern pattern, BoundExpression? guard, TAction action) + where TAction : class => new(pattern, guard, action); + + /// + /// Stringifies the given to a user-readable format. + /// + /// The pattern to stringify. + /// The user-readable form of . + public static string ToDisplayString(BoundPattern pattern) => pattern switch + { + BoundDiscardPattern => "_", + BoundLiteralPattern lit => lit.Value?.ToString() ?? "null", + _ => throw new ArgumentOutOfRangeException(nameof(pattern)), + }; +} + +/// +/// Represents a decision tree computed for a match expression. +/// It helps guiding the rewriter to generate code as well as reporting things like redundant branches or +/// missing cases. +/// +/// The action type being performed, when a branch is matched. +internal sealed class DecisionTree + where TAction : class +{ + /// + /// Builds a decision tree from the given data. + /// + /// The intrinsic symbols of the compilation. + /// The matched value. + /// The arms of the match. + /// The constructed decision tree. + public static DecisionTree Build(IntrinsicSymbols intrinsicSymbols, BoundExpression matchedValue, ImmutableArray arms) + { + // Construct root + var root = new Node( + parent: null, + arguments: new List { matchedValue }, + patternMatrix: arms + .Select(a => new List { a.Pattern }) + .ToList(), + actionArms: arms.ToList()); + // Wrap in the tree + var tree = new DecisionTree(intrinsicSymbols, root); + // Build it + tree.Build(root); + // Done + return tree; + } + + /// + /// A single arm in the match construct. + /// + /// The matched pattern. + /// The guard of the arm, if any. + /// The taken action if matches. + public readonly record struct Arm(BoundPattern Pattern, BoundExpression? Guard, TAction Action); + + /// + /// Represents a redundancy. + /// + /// The action that covers the one already. + /// The action that is redundant or useless, because already + /// matches. + public readonly record struct Redundance(TAction CoveredBy, TAction Useless); + + /// + /// Represents a conditional match by either a pattern or a guard condition. + /// + /// The pattern to match. + /// The guard to satisfy. + public readonly record struct Condition(BoundPattern? Pattern, BoundExpression? Guard) + { + /// + /// Default condition. + /// + public static Condition Else { get; } = new(); + + public static Condition Match(BoundPattern pattern) => new(pattern, null); + public static Condition If(BoundExpression expr) => new(null, expr); + } + + /// + /// A single node in the decision tree. + /// + public interface INode + { + /// + /// The parent node of this one, if any. + /// + public INode? Parent { get; } + + /// + /// True, if this is an action node, meaning that there is a match. + /// + [MemberNotNullWhen(true, nameof(ActionArm))] + [MemberNotNullWhen(true, nameof(Action))] + public bool IsAction { get; } + + /// + /// The action arm that's associated with the node, in case it's a leaf. + /// + public Arm? ActionArm { get; } + + /// + /// The action that's associated with the node, in case it's a leaf. + /// + public TAction? Action { get; } + + /// + /// True, if this is a failure node. + /// + public bool IsFail { get; } + + /// + /// An example pattern that this node did not cover. + /// + public BoundPattern? NotCovered { get; } + + /// + /// The expression being matched on. + /// + public BoundExpression MatchedValue { get; } + + /// + /// The child nodes of this one, associated to the pattern to match to take the route to the child. + /// + public IReadOnlyList> Children { get; } + } + + // A mutable node implementation + private sealed class Node : INode + { + public INode? Parent { get; } + public bool IsAction => this.ActionArm is not null; + public Arm? ActionArm { get; set; } + public TAction? Action => this.ActionArm?.Action; + public bool IsFail => this.PatternMatrix.Count == 0; + public BoundPattern? NotCovered { get; set; } + public BoundExpression MatchedValue => this.Arguments[0]; + public List> Children { get; } = new(); + IReadOnlyList> INode.Children => this.Children; + + public List Arguments { get; } + public List ArgumentOrder { get; } + public List> PatternMatrix { get; } + public List ActionArms { get; } + + public int FirstColumnWithRefutableEntry + { + get + { + for (var col = 0; col < this.PatternMatrix[0].Count; ++col) + { + if (this.PatternMatrix.Any(row => !MatchesEverything(row[col]))) return col; + } + + throw new InvalidOperationException("the matrix only contains irrefutable entries"); + } + } + + public Node( + INode? parent, + List arguments, + List> patternMatrix, + List actionArms) + { + this.Parent = parent; + this.Arguments = arguments; + this.ArgumentOrder = Enumerable + .Range(0, this.Arguments.Count) + .ToList(); + this.PatternMatrix = patternMatrix; + this.ActionArms = actionArms; + } + + public void SwapColumns(int i, int j) + { + Swap(this.Arguments, i, j); + Swap(this.ArgumentOrder, i, j); + foreach (var row in this.PatternMatrix) Swap(row, i, j); + } + + private static void Swap(List list, int i, int j) => (list[i], list[j]) = (list[j], list[i]); + } + + /// + /// A comparer that compares patterns only in terms of specialization, not involving their + /// arguments, if there are any. + /// + private sealed class SpecializationComparer : IEqualityComparer + { + /// + /// A singleton instance that can be used. + /// + public static SpecializationComparer Instance { get; } = new(); + + private SpecializationComparer() + { + } + + public bool Equals(BoundPattern? x, BoundPattern? y) => (x, y) switch + { + (BoundDiscardPattern, BoundDiscardPattern) => true, + (BoundLiteralPattern lit1, BoundLiteralPattern lit2) => Equals(lit1.Value, lit2.Value), + _ => false, + }; + + public int GetHashCode([DisallowNull] BoundPattern obj) => obj switch + { + BoundUnexpectedPattern => 0, + BoundDiscardPattern => 0, + BoundLiteralPattern lit => lit.Value?.GetHashCode() ?? 0, + _ => throw new ArgumentOutOfRangeException(nameof(obj)), + }; + } + + /// + /// Checks, if the given pattern matches any value, making it irrefutable. + /// + /// The pattern to check. + /// True, if matches any possible value. + private static bool MatchesEverything(BoundPattern pattern) => pattern switch + { + BoundDiscardPattern => true, + _ => false, + }; + + /// + /// Attempts to explode a pattern with its arguments for specialization. + /// + /// The specializing pattern. + /// The pattern to explode. + /// The exploded arguments of if it can be specialized by + /// , null otherwise. + private static ImmutableArray? TryExplode(BoundPattern specializer, BoundPattern toExplode) => (specializer, toExplode) switch + { + (BoundDiscardPattern, _) => throw new ArgumentOutOfRangeException(nameof(specializer)), + (BoundLiteralPattern, BoundDiscardPattern) => ImmutableArray.Empty, + (BoundLiteralPattern lit1, BoundLiteralPattern lit2) when Equals(lit1.Value, lit2.Value) => ImmutableArray.Empty, + _ => null, + }; + + // TODO: Doc + private static BoundPattern? ConstructUnhandledPattern(INode node) + { + if (node.NotCovered is null) return null; + + // TODO: Wrap in parent + return node.NotCovered; + } + + /// + /// The root node of this tree. + /// + public INode Root => this.root; + private readonly Node root; + + /// + /// All redundancies in the tree. + /// + public IEnumerable Redundancies + { + get + { + foreach (var (covered, coveringSet) in this.coveredBy) + { + if (this.usedActions.Contains(covered)) continue; + + foreach (var covering in coveringSet) + { + yield return new(CoveredBy: covering, Useless: covered); + } + } + } + } + + /// + /// True, if this tree is exhaustive. + /// + public bool IsExhaustive => GraphTraversal + .DepthFirst(this.Root, n => n.Children.Select(c => c.Value)) + .All(n => !n.IsFail); + + /// + /// An example of an unhandled pattern, if any. + /// + public BoundPattern? UnhandledExample => GraphTraversal + .DepthFirst(this.Root, n => n.Children.Select(c => c.Value)) + .Where(n => n.IsFail) + .Select(ConstructUnhandledPattern) + .OfType() + .FirstOrDefault(); + + private readonly IntrinsicSymbols intrinsicSymbols; + private readonly HashSet usedActions = new(); + private readonly Dictionary> coveredBy = new(); + + private DecisionTree(IntrinsicSymbols intrinsicSymbols, Node root) + { + this.intrinsicSymbols = intrinsicSymbols; + this.root = root; + } + + /// + /// Constructs a DOT graph representation of the decision-tree. + /// + /// Expression stringifier. + /// Pattern stringifier. + /// Action stringifier. + /// The DOT graph code of the decision-tree. + public string ToDot( + Func? exprToString = null, + Func? patToString = null, + Func? actToString = null) + { + exprToString ??= o => o.ToString(); + patToString ??= o => o.ToString(); + actToString ??= o => o.ToString(); + + var graph = new DotGraphBuilder(isDirected: true) + .WithRankDir(DotAttribs.RankDir.TopToBottom); + + void Recurse(INode node) + { + // Add node + graph + .AddVertex(node) + .WithLabel(node.IsAction ? actToString(node.Action) : exprToString(node.MatchedValue)); + // Add children and edges + foreach (var ((pat, cond), child) in node.Children) + { + var label = (pat, cond) switch + { + (BoundPattern, null) => $"match {patToString(pat)}", + (null, BoundExpression) => $"if {exprToString(cond)}", + (null, null) => "else", + _ => throw new InvalidOperationException(), + }; + graph + .AddEdge(node, child) + .WithLabel(label); + Recurse(child); + } + } + + Recurse(this.Root); + return graph.ToDot(); + } + + /// + /// Builds the subtree. + /// + /// The current root of the tree. + private void Build(Node node) + { + if (node.IsFail) return; + + if (node.PatternMatrix[0].All(MatchesEverything)) + { + // We can potentially match, all depends on the guard now + var takenAction = node.ActionArms[0]; + if (takenAction.Guard is null) + { + // This is a succeeding node, set the performed action + node.ActionArm = takenAction; + this.usedActions.Add(takenAction.Action); + // The remaining ones are registered as covered + for (var i = 1; i < node.PatternMatrix.Count; ++i) + { + var coveredAction = node.ActionArms[i].Action; + this.Cover(takenAction.Action, coveredAction); + } + } + else + { + // We still have a guard to match + // Split off first row + var (first, rest) = this.SplitOffFirstRow(node); + + // We can match the first one by the condition + first.ActionArm = takenAction; + this.usedActions.Add(takenAction.Action); + node.Children.Add(new(Condition.If(takenAction.Guard), first)); + + // Handle rest + node.Children.Add(new(Condition.Else, rest)); + this.Build(rest); + } + return; + } + + // We need to make a decision, bring the column that has refutable entries to the beginning + var firstColWithRefutable = node.FirstColumnWithRefutableEntry; + if (firstColWithRefutable != 0) node.SwapColumns(0, firstColWithRefutable); + + // The first column now contains something that is refutable + // Collect all pattern variants that we covered + var coveredPatterns = node.PatternMatrix.Zip(node.ActionArms) + .Select(pair => (Row: pair.First[0], Guard: pair.Second.Guard)) + .Where(p => !MatchesEverything(p.Row)) + .GroupBy(p => p.Row, SpecializationComparer.Instance); + + // Track if there are any uncovered values in this domain + var uncoveredDomain = ValueDomain.CreateDomain(this.intrinsicSymbols, node.PatternMatrix[0][0].Type); + + // Specialize for each of these cases + foreach (var group in coveredPatterns) + { + var pat = group.Key; + var guards = group.Select(p => p.Guard).Distinct(); + foreach (var guard in guards) + { + // Specialize to the pattern + var child = this.Specialize(node, pat); + // Add as child + node.Children.Add(new(Condition.Match(pat), child)); + // We only add covered, if it's not guarded + if (guard is null) + { + // We covered the value, subtract + uncoveredDomain.SubtractPattern(pat); + } + } + } + + // If not complete, do defaulting + if (!uncoveredDomain.IsEmpty) + { + var @default = this.Default(node); + // Add as child + node.Children.Add(new(Condition.Else, @default)); + // If it's a failure node, add counterexample + if (@default.IsFail) @default.NotCovered = uncoveredDomain.SamplePattern(); + } + + // Recurse to children + foreach (var (_, child) in node.Children) this.Build((Node)child); + } + + /// + /// Splits off the first row of the pattern matrix to produce a single-row node + /// and a node with everything else. + /// + /// The node to spli. + /// The pair of first-row and rest nodes. + private (Node First, Node Remaining) SplitOffFirstRow(Node node) + { + var first = new Node( + parent: node, + arguments: node.Arguments.ToList(), + patternMatrix: new List> { node.PatternMatrix[0].ToList() }, + actionArms: new List { node.ActionArms[0] }); + var rest = new Node( + parent: node, + arguments: node.Arguments.ToList(), + patternMatrix: node.PatternMatrix.Skip(1).ToList(), + actionArms: node.ActionArms.Skip(1).ToList()); + return (first, rest); + } + + /// + /// Specializes the given node. + /// + /// The node to specialize. + /// The specializer pattern. + /// The specialized by . + private Node Specialize(Node node, BoundPattern specializer) + { + var remainingRows = new List>(); + var remainingActions = new List(); + var hadEmptyRow = false; + foreach (var (row, action) in node.PatternMatrix.Zip(node.ActionArms)) + { + var exploded = TryExplode(specializer, row[0]); + if (exploded is null) continue; + + var newRow = exploded.Concat(row.Skip(1)).ToList(); + if (newRow.Count == 0) + { + hadEmptyRow = true; + newRow.Add(BoundDiscardPattern.Default); + } + + // NOTE: Row is already cloned + remainingRows.Add(newRow); + remainingActions.Add(action); + } + + if (remainingRows.Count == 0) + { + // Short-circuit to avoid errors + return new( + parent: node, + arguments: new(), + patternMatrix: new(), + actionArms: new()); + } + + var newArguments = Enumerable + .Range(0, remainingRows[0].Count - node.PatternMatrix[0].Count + 1) + .Select(i => hadEmptyRow + ? node.Arguments[0] + // TODO: Construct accessor for i-th element + : throw new NotImplementedException("we don't handle accessor construction yet")) + .Concat(node.Arguments.Skip(1)) + .ToList(); + + return new( + parent: node, + arguments: newArguments, + patternMatrix: remainingRows, + actionArms: remainingActions); + } + + /// + /// Constructs the default node, which keeps only the irrefutable elements. + /// + /// The node to construct the default of. + /// The defaulted . + private Node Default(Node node) + { + // Keep only irrefutable rows + var remainingRows = new List>(); + var remainingActions = new List(); + foreach (var (row, action) in node.PatternMatrix.Zip(node.ActionArms)) + { + if (!MatchesEverything(row[0])) continue; + + // NOTE: We need to clone in case it gets mutated + remainingRows.Add(row.ToList()); + remainingActions.Add(action); + } + return new( + parent: node, + arguments: node.Arguments, + patternMatrix: remainingRows, + actionArms: remainingActions); + } + + private void Cover(TAction covering, TAction covered) + { + if (!this.coveredBy.TryGetValue(covered, out var coverSet)) + { + coverSet = new(); + this.coveredBy.Add(covered, coverSet); + } + coverSet.Add(covering); + } +} diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/Domain/EmptyDomain.cs b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/EmptyDomain.cs new file mode 100644 index 000000000..709b22f70 --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/EmptyDomain.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Draco.Compiler.Internal.BoundTree; + +namespace Draco.Compiler.Internal.FlowAnalysis.Domain; + +/// +/// Represents a domain that's empty. +/// +internal sealed class EmptyDomain : ValueDomain +{ + /// + /// A singleton instance to use. + /// + public static EmptyDomain Instance { get; } = new(); + + public override bool IsEmpty => true; + + private EmptyDomain() + { + } + + public override ValueDomain Clone() => Instance; + public override void SubtractPattern(BoundPattern pattern) { } + public override BoundPattern? SamplePattern() => null; + public override string ToString() => ""; +} diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/Domain/FullDomain.cs b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/FullDomain.cs new file mode 100644 index 000000000..39e800078 --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/FullDomain.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Draco.Compiler.Internal.BoundTree; + +namespace Draco.Compiler.Internal.FlowAnalysis.Domain; + +/// +/// A domain that is always full. +/// +internal sealed class FullDomain : ValueDomain +{ + /// + /// A singleton instance to use. + /// + public static FullDomain Instance { get; } = new(); + + public override bool IsEmpty => false; + + private FullDomain() + { + } + + public override ValueDomain Clone() => Instance; + public override void SubtractPattern(BoundPattern pattern) { } + public override BoundPattern? SamplePattern() => null; + public override string ToString() => ""; +} diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/Domain/IntegralDomain.cs b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/IntegralDomain.cs new file mode 100644 index 000000000..c812077af --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/IntegralDomain.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.Symbols; +using Draco.Compiler.Internal.Utilities; + +namespace Draco.Compiler.Internal.FlowAnalysis.Domain; + +/// +/// Any integral number domain. +/// +/// The integral value type. +internal sealed class IntegralDomain : ValueDomain + where TInteger : struct, + IComparisonOperators, + IAdditionOperators, + ISubtractionOperators, + IAdditiveIdentity, + IMultiplicativeIdentity +{ + /// + /// A continuous interval of integral values. + /// + /// The lower bound (inclusive). + /// The upper bound (inclusive). + private readonly record struct Interval(TInteger From, TInteger To) + { + public override string ToString() => $"[{this.From}; {this.To}]"; + } + + public override bool IsEmpty => + this.subtracted.Count == 1 + && this.subtracted[0].From == this.minValue + && this.subtracted[0].To == this.maxValue; + + private readonly TypeSymbol backingType; + private readonly TInteger minValue; + private readonly TInteger maxValue; + private readonly List subtracted; + + private IntegralDomain( + TypeSymbol backingType, + TInteger minValue, + TInteger maxValue, + List subtracted) + { + this.backingType = backingType; + this.minValue = minValue; + this.maxValue = maxValue; + this.subtracted = subtracted; + } + + public IntegralDomain(TypeSymbol backingType, TInteger minValue, TInteger maxValue) + : this(backingType, minValue, maxValue, new()) + { + } + + public override ValueDomain Clone() => + new IntegralDomain(this.backingType, this.minValue, this.maxValue, this.subtracted.ToList()); + + public override void SubtractPattern(BoundPattern pattern) + { + switch (pattern) + { + case BoundDiscardPattern: + // Subtract everything + this.SubtractRange(this.minValue, this.maxValue); + break; + case BoundLiteralPattern litPattern when litPattern.Value is TInteger num: + // Subtract singular value + this.SubtractRange(num, num); + break; + default: + throw new ArgumentException("illegal pattern for integral domain", nameof(pattern)); + } + } + + public override BoundPattern? SamplePattern() + { + var sample = this.SampleValue(); + return sample is null ? null : this.ToPattern(sample.Value); + } + + public TInteger? SampleValue() + { + // Special case: entire domain covered + if (this.IsEmpty) return null; + + // Search for one around zero + var span = (ReadOnlySpan)CollectionsMarshal.AsSpan(this.subtracted); + var (index, _) = BinarySearch.Search(span, TInteger.AdditiveIdentity, i => i.From); + + // NOTE: This is a very stupid implementation, but hopefully better than working out the edge cases + const int surroundCheck = 1; + var min = Math.Max(index - surroundCheck, 0); + var max = Math.Min(index + surroundCheck, span.Length - 1); + for (var i = max; i >= min; --i) + { + var interval = span[i]; + if (interval.To != this.maxValue) return interval.To + TInteger.MultiplicativeIdentity; + if (interval.From != this.minValue) return interval.From - TInteger.MultiplicativeIdentity; + } + + // NOTE: Should never happen + return null; + } + + public override string ToString() + { + if (this.IsEmpty) return "empty"; + + var parts = new List(); + if (this.subtracted[0].From != this.minValue) parts.Add($"[{this.minValue}; {this.subtracted[0].From})"); + for (var i = 0; i < this.subtracted.Count - 1; ++i) + { + parts.Add($"({this.subtracted[i].To}; {this.subtracted[i + 1].From})"); + } + if (this.subtracted[^1].To != this.maxValue) parts.Add($"({this.subtracted[^1].To}; {this.maxValue}]"); + return string.Join(" U ", parts); + } + + private void SubtractRange(TInteger from, TInteger to) + { + if (from > to) throw new ArgumentOutOfRangeException(nameof(to), "from must be less than or equal to to"); + + var span = (ReadOnlySpan)CollectionsMarshal.AsSpan(this.subtracted); + var newInterval = new Interval(from, to); + + if (span.Length == 0) + { + // Simple, first addition + this.subtracted.Add(new(from, to)); + return; + } + + var (startIndex, _) = BinarySearch.Search(span, from, i => i.From); + var (endIndex, _) = BinarySearch.Search(span, to, i => i.To); + + // NOTE: This is a very stupid implementation, but hopefully better than working out the edge cases + const int surroundCheck = 1; + var startErase = -1; + var endErase = -1; + // Start + { + var min = Math.Max(startIndex - surroundCheck, 0); + var max = Math.Min(startIndex + surroundCheck, span.Length - 1); + for (var i = min; i <= max; ++i) + { + if (!Intersects(span[i], newInterval)) continue; + startErase = i; + break; + } + } + // End + { + var max = Math.Min(endIndex + surroundCheck, span.Length - 1); + var min = Math.Max(endIndex - surroundCheck, 0); + for (var i = max; i >= min; --i) + { + if (!Intersects(span[i], newInterval)) continue; + endErase = i; + break; + } + } + + // If negative, no need to erase + if (startErase < 0) + { + Debug.Assert(startIndex == endIndex); + Debug.Assert(endErase < 0); + this.subtracted.Insert(startIndex, newInterval); + return; + } + + Debug.Assert(endErase >= 0); + + // Expand from and to as needed + from = Min(from, span[startErase].From); + to = Max(to, span[endErase].To); + + // Erase + this.subtracted.RemoveRange(startErase, endErase - startErase + 1); + // Insert + this.subtracted.Insert(startErase, new(from, to)); + } + + private BoundPattern ToPattern(TInteger integer) => + new BoundLiteralPattern(null, integer, this.backingType); + + private static bool Intersects(Interval a, Interval b) => + Touches(a, b) + || Contains(a, b.From) + || Contains(a, b.To) + || Contains(b, a.From) + || Contains(b, a.To); + private static bool Contains(Interval iv, TInteger n) => iv.From <= n && n <= iv.To; + private static bool Touches(Interval a, Interval b) => + a.To + TInteger.MultiplicativeIdentity == b.From + || b.To + TInteger.MultiplicativeIdentity == a.From; + private static TInteger Min(TInteger a, TInteger b) => a > b ? b : a; + private static TInteger Max(TInteger a, TInteger b) => a > b ? a : b; +} diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/Domain/StringDomain.cs b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/StringDomain.cs new file mode 100644 index 000000000..42a7cecc6 --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/StringDomain.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.Symbols; +using Draco.Compiler.Internal.Utilities; + +namespace Draco.Compiler.Internal.FlowAnalysis.Domain; + +internal sealed class StringDomain : ValueDomain +{ + public override bool IsEmpty => this.allSubtracted; + + private readonly TypeSymbol backingType; + private readonly HashSet subtracted; + private bool allSubtracted; + + private StringDomain(TypeSymbol backingType, HashSet subtracted, bool allSubtracted) + { + this.backingType = backingType; + this.subtracted = subtracted; + this.allSubtracted = allSubtracted; + } + + public StringDomain(TypeSymbol backingType) + : this(backingType, new(), false) + { + } + + public override string ToString() => this.allSubtracted + ? "empty" + : $"Universe \\ {{{string.Join(", ", this.subtracted.Select(StringUtils.Unescape))}}}"; + + public override ValueDomain Clone() => + new StringDomain(this.backingType, this.subtracted.ToHashSet(), this.allSubtracted); + + public override BoundPattern? SamplePattern() + { + if (this.allSubtracted) return null; + + // Check some trivial ones + if (!this.subtracted.Contains(string.Empty)) return this.ToPattern(string.Empty); + + // We use the same trick as proving that you can't list all possible reals + // We construct a string that differs in the first char from the first string, + // in the second char of the second string, ... + + var result = new StringBuilder(); + var i = 0; + foreach (var str in this.subtracted) + { + var ithChar = str.Length < i ? str[i] : '\0'; + result.Append(ithChar == 'a' ? 'b' : 'a'); + } + + return this.ToPattern(result.ToString()); + } + + public override void SubtractPattern(BoundPattern pattern) + { + if (this.allSubtracted) return; + + switch (pattern) + { + case BoundDiscardPattern: + this.subtracted.Clear(); + this.allSubtracted = true; + break; + case BoundLiteralPattern litPattern when litPattern.Value is string str: + this.subtracted.Add(str); + break; + default: + throw new ArgumentException("illegal pattern for string domain", nameof(pattern)); + } + } + + private BoundPattern ToPattern(string text) => throw new NotImplementedException(); +} diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/Domain/ValueDomain.cs b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/ValueDomain.cs new file mode 100644 index 000000000..6ca40af9e --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/Domain/ValueDomain.cs @@ -0,0 +1,54 @@ +using System; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.Symbols; +using Draco.Compiler.Internal.Symbols.Synthetized; + +namespace Draco.Compiler.Internal.FlowAnalysis.Domain; + +/// +/// Represents some domain of values in the compiler. +/// +internal abstract class ValueDomain +{ + /// + /// Creates a domain for the given . + /// + /// The intrinsics for the compilation. + /// The type to create the domain for. + /// The domain representing . + public static ValueDomain CreateDomain(IntrinsicSymbols intrinsics, TypeSymbol type) + { + if (SymbolEqualityComparer.Default.Equals(type, intrinsics.Int32)) + { + return new IntegralDomain(type, int.MinValue, int.MaxValue); + } + + return FullDomain.Instance; + } + + /// + /// True, if this domain has been emptied. + /// + public abstract bool IsEmpty { get; } + + /// + /// Clones this domain. + /// + /// The clone of this domain. + public abstract ValueDomain Clone(); + + /// + /// Removes the given pattern from the domain. + /// + /// The pattern to remove. + public abstract void SubtractPattern(BoundPattern pattern); + + /// + /// Retrieves a sample value from the domain that has not been covered yet. + /// + /// A pattern representing an uncovered value, or null, if the domain has been emptied + /// or it cannot provide a value (because the domain is open for example). + public abstract BoundPattern? SamplePattern(); + + public override abstract string ToString(); +} diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/FlowAnalysisErrors.cs b/src/Draco.Compiler/Internal/FlowAnalysis/FlowAnalysisErrors.cs index 198c72c03..939796494 100644 --- a/src/Draco.Compiler/Internal/FlowAnalysis/FlowAnalysisErrors.cs +++ b/src/Draco.Compiler/Internal/FlowAnalysis/FlowAnalysisErrors.cs @@ -55,4 +55,22 @@ internal static class FlowAnalysisErrors severity: DiagnosticSeverity.Error, format: "the immutable variable {0} can not be assigned to, it is read only", code: Code(5)); + + /// + /// A match expression is non-exhaustive. + /// + public static readonly DiagnosticTemplate NonExhaustiveMatchExpression = DiagnosticTemplate.Create( + title: "match expression is not exhaustive", + severity: DiagnosticSeverity.Warning, + format: "the match expression does not cover all possible values", + code: Code(6)); + + /// + /// A match expression arm is already covered by another. + /// + public static readonly DiagnosticTemplate MatchPatternAlreadyHandled = DiagnosticTemplate.Create( + title: "pattern already handled", + severity: DiagnosticSeverity.Warning, + format: "the pattern is already handled by a previous case", + code: Code(7)); } diff --git a/src/Draco.Compiler/Internal/FlowAnalysis/MatchExhaustiveness.cs b/src/Draco.Compiler/Internal/FlowAnalysis/MatchExhaustiveness.cs new file mode 100644 index 000000000..56853d75e --- /dev/null +++ b/src/Draco.Compiler/Internal/FlowAnalysis/MatchExhaustiveness.cs @@ -0,0 +1,78 @@ +using System.Collections.Immutable; +using System.Linq; +using Draco.Compiler.Api.Diagnostics; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.Diagnostics; +using Draco.Compiler.Internal.Symbols.Source; +using Draco.Compiler.Internal.Symbols.Synthetized; + +namespace Draco.Compiler.Internal.FlowAnalysis; + +/// +/// Checks, if match expressions are exhaustive. +/// +internal sealed class MatchExhaustiveness : BoundTreeVisitor +{ + public static void Analyze(SourceGlobalSymbol global, IntrinsicSymbols intrinsicSymbols, DiagnosticBag diagnostics) + { + var pass = new MatchExhaustiveness(intrinsicSymbols, diagnostics); + global.Value?.Accept(pass); + } + + public static void Analyze(SourceFunctionSymbol function, IntrinsicSymbols intrinsicSymbols, DiagnosticBag diagnostics) + { + var pass = new MatchExhaustiveness(intrinsicSymbols, diagnostics); + function.Body.Accept(pass); + } + + private readonly IntrinsicSymbols intrinsicSymbols; + private readonly DiagnosticBag diagnostics; + + public MatchExhaustiveness(IntrinsicSymbols intrinsicSymbols, DiagnosticBag diagnostics) + { + this.intrinsicSymbols = intrinsicSymbols; + this.diagnostics = diagnostics; + } + + public override void VisitMatchExpression(BoundMatchExpression node) + { + base.VisitMatchExpression(node); + + // We build up the relevant arms + var arms = node.MatchArms + .Select(a => DecisionTree.Arm(a.Pattern, a.Guard, a)) + .ToImmutableArray(); + // From that we build the decision tree + var decisionTree = DecisionTree.Build(this.intrinsicSymbols, node.MatchedValue, arms); + + if (!decisionTree.IsExhaustive) + { + // Report + var diagBuilder = Diagnostic.CreateBuilder() + .WithTemplate(FlowAnalysisErrors.NonExhaustiveMatchExpression) + .WithLocation(node.Syntax?.Location); + var example = decisionTree.UnhandledExample; + if (example is not null) + { + diagBuilder.WithRelatedInformation(DiagnosticRelatedInformation.Create( + location: node.Syntax?.Location, + format: "for example, the pattern {0} is not handled", + formatArgs: DecisionTree.ToDisplayString(example))); + } + this.diagnostics.Add(diagBuilder.Build()); + } + + foreach (var group in decisionTree.Redundancies.GroupBy(r => r.Useless)) + { + // Report + this.diagnostics.Add(Diagnostic.Create( + template: FlowAnalysisErrors.MatchPatternAlreadyHandled, + location: group.Key.Syntax?.Location, + relatedInformation: group + .Select(cover => DiagnosticRelatedInformation.Create( + location: cover.CoveredBy.Syntax?.Location, + "a case covering this one can be found here")) + .ToImmutableArray())); + } + } +} diff --git a/src/Draco.Compiler/Internal/Lowering/LocalRewriter.cs b/src/Draco.Compiler/Internal/Lowering/LocalRewriter.cs index 1888e2289..e744ba601 100644 --- a/src/Draco.Compiler/Internal/Lowering/LocalRewriter.cs +++ b/src/Draco.Compiler/Internal/Lowering/LocalRewriter.cs @@ -15,7 +15,7 @@ namespace Draco.Compiler.Internal.Lowering; /// /// Performs local rewrites of the source code. /// -internal partial class LocalRewriter : BoundTreeRewriter +internal sealed partial class LocalRewriter : BoundTreeRewriter { /// /// Represents a value that was temporarily stored. diff --git a/src/Draco.Compiler/Internal/Lowering/LocalRewriter_MatchExpression.cs b/src/Draco.Compiler/Internal/Lowering/LocalRewriter_MatchExpression.cs new file mode 100644 index 000000000..b9d3b6284 --- /dev/null +++ b/src/Draco.Compiler/Internal/Lowering/LocalRewriter_MatchExpression.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.FlowAnalysis; +using Draco.Compiler.Internal.Symbols; +using static Draco.Compiler.Internal.BoundTree.BoundTreeFactory; + +namespace Draco.Compiler.Internal.Lowering; + +internal sealed partial class LocalRewriter +{ + public override BoundNode VisitMatchExpression(BoundMatchExpression node) + { + // TODO: Elaborate on what we do here + + // Evaluate the matched value as a local to not duplicate side-effects + var matchedValue = (BoundExpression)node.MatchedValue.Accept(this); + var tmp = this.StoreTemporary(matchedValue); + + // We build up the relevant arms + var arms = node.MatchArms + .Select(a => DecisionTree.Arm(a.Pattern, a.Guard, a)) + .ToImmutableArray(); + // From that we build the decision tree + var decisionTree = DecisionTree.Build(this.IntrinsicSymbols, tmp.Reference, arms); + + // Recursively lower each decision node + var decisionNode = this.ConstructMatchNode(decisionTree.Root, node.TypeRequired); + + var result = BlockExpression( + locals: tmp.Symbol is null + ? ImmutableArray.Empty + : ImmutableArray.Create(tmp.Symbol), + statements: ImmutableArray.Create(tmp.Assignment), + decisionNode); + return result.Accept(this); + } + + private BoundExpression ConstructMatchNode(DecisionTree.INode node, TypeSymbol resultType) + { + // Hit a leaf + if (node.IsAction) return (BoundExpression)node.Action.Value.Accept(this); + + // TODO + // Failure + if (node.IsFail) throw new NotImplementedException(); + + // Fold backwards + var seed = null as BoundExpression; + for (var i = node.Children.Count - 1; i >= 0; --i) + { + var (cond, child) = node.Children[i]; + seed = Fold(seed, cond, child); + } + + Debug.Assert(seed is not null); + return seed; + + BoundExpression Fold( + BoundExpression? prev, + DecisionTree.Condition condition, + DecisionTree.INode node) + { + var matchedValue = (BoundExpression)node.MatchedValue; + var result = this.ConstructMatchNode(node, resultType); + switch (condition.Pattern, condition.Guard) + { + case (null, null): + { + Debug.Assert(prev is null); + return result; + } + case (BoundPattern pat, null): + { + var matchCondition = this.ConstructPatternToMatch(pat, matchedValue); + return IfExpression( + condition: matchCondition, + then: result, + @else: prev ?? DefaultExpression(), + type: resultType); + } + case (null, BoundExpression cond): + { + return IfExpression( + condition: cond, + then: result, + @else: prev ?? DefaultExpression(), + type: resultType); + } + default: + throw new InvalidOperationException(); + } + } + + // TODO: As a default, we should THROW + static BoundExpression DefaultExpression() => BoundUnitExpression.Default; + } + + private BoundExpression ConstructPatternToMatch(BoundPattern pattern, BoundExpression matchedValue) => pattern switch + { + BoundDiscardPattern discard => this.ConstructPatternToMatch(discard, matchedValue), + BoundLiteralPattern literal => this.ConstructPatternToMatch(literal, matchedValue), + _ => throw new ArgumentOutOfRangeException(nameof(pattern)), + }; + + private BoundExpression ConstructPatternToMatch(BoundDiscardPattern pattern, BoundExpression matchedValue) => + // _ matches expr + // + // => + // + // true + this.LiteralExpression(true); + + private BoundExpression ConstructPatternToMatch(BoundLiteralPattern pattern, BoundExpression matchedValue) => + // N matches expr + // + // => + // + // object.Equals(N, expr) + CallExpression( + receiver: null, + method: this.WellKnownTypes.SystemObject_Equals, + arguments: ImmutableArray.Create( + this.LiteralExpression(pattern.Value), + matchedValue)); +} diff --git a/src/Draco.Compiler/Internal/OptimizingIr/AssemblyCodegen.cs b/src/Draco.Compiler/Internal/OptimizingIr/AssemblyCodegen.cs index 01c083690..a2743dcb8 100644 --- a/src/Draco.Compiler/Internal/OptimizingIr/AssemblyCodegen.cs +++ b/src/Draco.Compiler/Internal/OptimizingIr/AssemblyCodegen.cs @@ -40,7 +40,7 @@ private void Complete() { // Set the entry point, in case we have one var mainProcedure = (Procedure?)this.assembly.RootModule.Procedures.Values - .FirstOrDefault(p => p.Name == CompilerConstants.EntryPointName); + .FirstOrDefault(p => p.Name == Constants.EntryPointName); this.assembly.EntryPoint = mainProcedure; } } diff --git a/src/Draco.Compiler/Internal/Symbols/FunctionSymbol.cs b/src/Draco.Compiler/Internal/Symbols/FunctionSymbol.cs index a44f93ab2..76f4740f6 100644 --- a/src/Draco.Compiler/Internal/Symbols/FunctionSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/FunctionSymbol.cs @@ -3,6 +3,7 @@ using System.Text; using Draco.Compiler.Api.Syntax; using Draco.Compiler.Internal.Symbols.Generic; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols; @@ -89,8 +90,7 @@ public override Api.Semantics.Visibility Visibility { get { - var syntax = this.DeclaringSyntax as FunctionDeclarationSyntax; - if (syntax is null) return Api.Semantics.Visibility.Internal; // Default + if (this.DeclaringSyntax is not FunctionDeclarationSyntax syntax) return Api.Semantics.Visibility.Internal; // Default return GetVisibilityFromTokenKind(syntax.VisibilityModifier?.Kind); } } diff --git a/src/Draco.Compiler/Internal/Symbols/Generic/FieldInstanceSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Generic/FieldInstanceSymbol.cs index 75e92131e..2b64fd918 100644 --- a/src/Draco.Compiler/Internal/Symbols/Generic/FieldInstanceSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Generic/FieldInstanceSymbol.cs @@ -1,3 +1,5 @@ +using Draco.Compiler.Internal.Utilities; + namespace Draco.Compiler.Internal.Symbols.Generic; /// diff --git a/src/Draco.Compiler/Internal/Symbols/Generic/FunctionInstanceSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Generic/FunctionInstanceSymbol.cs index f82022991..32b05fd90 100644 --- a/src/Draco.Compiler/Internal/Symbols/Generic/FunctionInstanceSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Generic/FunctionInstanceSymbol.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Linq; using System.Text; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Generic; diff --git a/src/Draco.Compiler/Internal/Symbols/Generic/ParameterInstanceSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Generic/ParameterInstanceSymbol.cs index f1e425b33..59758e9cf 100644 --- a/src/Draco.Compiler/Internal/Symbols/Generic/ParameterInstanceSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Generic/ParameterInstanceSymbol.cs @@ -1,3 +1,5 @@ +using Draco.Compiler.Internal.Utilities; + namespace Draco.Compiler.Internal.Symbols.Generic; /// diff --git a/src/Draco.Compiler/Internal/Symbols/Generic/PropertyInstanceSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Generic/PropertyInstanceSymbol.cs index f6b1fd049..4b52e0e8c 100644 --- a/src/Draco.Compiler/Internal/Symbols/Generic/PropertyInstanceSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Generic/PropertyInstanceSymbol.cs @@ -1,3 +1,5 @@ +using Draco.Compiler.Internal.Utilities; + namespace Draco.Compiler.Internal.Symbols.Generic; /// diff --git a/src/Draco.Compiler/Internal/Symbols/Generic/TypeInstanceSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Generic/TypeInstanceSymbol.cs index 8f3cdecda..7384b08d6 100644 --- a/src/Draco.Compiler/Internal/Symbols/Generic/TypeInstanceSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Generic/TypeInstanceSymbol.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Linq; using System.Text; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Generic; diff --git a/src/Draco.Compiler/Internal/Symbols/MergedModuleSymbol.cs b/src/Draco.Compiler/Internal/Symbols/MergedModuleSymbol.cs index 848dfc3bf..310337e47 100644 --- a/src/Draco.Compiler/Internal/Symbols/MergedModuleSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/MergedModuleSymbol.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataAssemblySymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataAssemblySymbol.cs index b606dde19..81a7d3d55 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataAssemblySymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataAssemblySymbol.cs @@ -4,6 +4,7 @@ using System.Reflection.Metadata; using System.Xml; using Draco.Compiler.Api; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataFieldSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataFieldSymbol.cs index 51c5d54c5..6526e37f0 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataFieldSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataFieldSymbol.cs @@ -3,6 +3,7 @@ using System.Reflection.Metadata; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs index ee6f6106a..58e2cb041 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs @@ -6,6 +6,7 @@ using System.Threading; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataNamespaceSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataNamespaceSymbol.cs index 9a19e1998..ca74989e3 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataNamespaceSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataNamespaceSymbol.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Metadata; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataPropertySymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataPropertySymbol.cs index 58f518a55..66f20586a 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataPropertySymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataPropertySymbol.cs @@ -3,6 +3,7 @@ using System.Reflection.Metadata; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; @@ -101,9 +102,7 @@ private void BuildOverride() private PropertySymbol? GetExplicitOverride() { - var accessor = this.Getter ?? this.Setter; - if (accessor is null) throw new InvalidOperationException(); - + var accessor = this.Getter ?? this.Setter ?? throw new InvalidOperationException(); if (accessor.Override is not null) return (accessor.Override as IPropertyAccessorSymbol)?.Property; return null; } diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticClassSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticClassSymbol.cs index 3014fb2e0..9f6dfcf26 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticClassSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticClassSymbol.cs @@ -5,6 +5,7 @@ using System.Reflection.Metadata; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticFieldSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticFieldSymbol.cs index 7fa3cc852..7f07c21bc 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticFieldSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataStaticFieldSymbol.cs @@ -3,6 +3,7 @@ using System.Reflection.Metadata; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataTypeSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataTypeSymbol.cs index ba8ff96f0..a6c99f1a2 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataTypeSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataTypeSymbol.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Metadata; diff --git a/src/Draco.Compiler/Internal/Symbols/Source/SourceFunctionSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Source/SourceFunctionSymbol.cs index 4a934edac..d4f5b311b 100644 --- a/src/Draco.Compiler/Internal/Symbols/Source/SourceFunctionSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Source/SourceFunctionSymbol.cs @@ -10,6 +10,7 @@ using Draco.Compiler.Internal.Documentation.Extractors; using Draco.Compiler.Internal.FlowAnalysis; using Draco.Compiler.Internal.Symbols.Synthetized; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Source; @@ -70,6 +71,7 @@ public void Bind(IBinderProvider binderProvider) ReturnsOnAllPaths.Analyze(this, binderProvider.DiagnosticBag); DefiniteAssignment.Analyze(body, binderProvider.DiagnosticBag); ValAssignment.Analyze(this, binderProvider.DiagnosticBag); + MatchExhaustiveness.Analyze(this, this.DeclaringCompilation!.IntrinsicSymbols, binderProvider.DiagnosticBag); } private void CheckForSameParameterOverloads(IBinderProvider binderProvider) diff --git a/src/Draco.Compiler/Internal/Symbols/Source/SourceGlobalSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Source/SourceGlobalSymbol.cs index 428c64020..5ade14673 100644 --- a/src/Draco.Compiler/Internal/Symbols/Source/SourceGlobalSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Source/SourceGlobalSymbol.cs @@ -6,6 +6,7 @@ using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; using Draco.Compiler.Internal.FlowAnalysis; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Source; @@ -49,6 +50,7 @@ public void Bind(IBinderProvider binderProvider) // Flow analysis if (value is not null) DefiniteAssignment.Analyze(value, binderProvider.DiagnosticBag); ValAssignment.Analyze(this, binderProvider.DiagnosticBag); + MatchExhaustiveness.Analyze(this, this.DeclaringCompilation!.IntrinsicSymbols, binderProvider.DiagnosticBag); } private (TypeSymbol Type, BoundExpression? Value) BindTypeAndValueIfNeeded(IBinderProvider binderProvider) diff --git a/src/Draco.Compiler/Internal/Symbols/Source/SourceLocalSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Source/SourceLocalSymbol.cs index 55f1bebea..9a13d0ffa 100644 --- a/src/Draco.Compiler/Internal/Symbols/Source/SourceLocalSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Source/SourceLocalSymbol.cs @@ -2,6 +2,7 @@ using Draco.Compiler.Internal.Binding; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Source; diff --git a/src/Draco.Compiler/Internal/Symbols/Source/SourceModuleSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Source/SourceModuleSymbol.cs index c5dcfa9cd..a7ce17906 100644 --- a/src/Draco.Compiler/Internal/Symbols/Source/SourceModuleSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Source/SourceModuleSymbol.cs @@ -10,6 +10,7 @@ using Draco.Compiler.Internal.Declarations; using Draco.Compiler.Internal.Documentation; using Draco.Compiler.Internal.Documentation.Extractors; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Source; diff --git a/src/Draco.Compiler/Internal/Symbols/Source/SourceParameterSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Source/SourceParameterSymbol.cs index a4cabdd0f..6e6956f98 100644 --- a/src/Draco.Compiler/Internal/Symbols/Source/SourceParameterSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Source/SourceParameterSymbol.cs @@ -1,6 +1,7 @@ using Draco.Compiler.Api.Diagnostics; using Draco.Compiler.Api.Syntax; using Draco.Compiler.Internal.Binding; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Source; diff --git a/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayConstructorSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayConstructorSymbol.cs index 94085e817..819cdb964 100644 --- a/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayConstructorSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayConstructorSymbol.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Linq; using Draco.Compiler.Internal.BoundTree; +using Draco.Compiler.Internal.Utilities; using static Draco.Compiler.Internal.BoundTree.BoundTreeFactory; namespace Draco.Compiler.Internal.Symbols.Synthetized; diff --git a/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayIndexPropertySymbol.cs b/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayIndexPropertySymbol.cs index c71606bb8..7257c2a4c 100644 --- a/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayIndexPropertySymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Synthetized/ArrayIndexPropertySymbol.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Linq; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols.Synthetized; diff --git a/src/Draco.Compiler/Internal/Symbols/Synthetized/IntrinsicSymbols.cs b/src/Draco.Compiler/Internal/Symbols/Synthetized/IntrinsicSymbols.cs index 523d72661..578b57c3a 100644 --- a/src/Draco.Compiler/Internal/Symbols/Synthetized/IntrinsicSymbols.cs +++ b/src/Draco.Compiler/Internal/Symbols/Synthetized/IntrinsicSymbols.cs @@ -4,6 +4,8 @@ using Draco.Compiler.Api.Syntax; using Draco.Compiler.Internal.OptimizingIr; using Draco.Compiler.Internal.OptimizingIr.Model; +using Draco.Compiler.Internal.Symbols.Error; +using Draco.Compiler.Internal.Utilities; using static Draco.Compiler.Internal.OptimizingIr.InstructionFactory; namespace Draco.Compiler.Internal.Symbols.Synthetized; diff --git a/src/Draco.Compiler/Internal/Symbols/Synthetized/MetadataConstructorFunctionSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Synthetized/MetadataConstructorFunctionSymbol.cs index c7a548f36..138c4a345 100644 --- a/src/Draco.Compiler/Internal/Symbols/Synthetized/MetadataConstructorFunctionSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Synthetized/MetadataConstructorFunctionSymbol.cs @@ -3,6 +3,7 @@ using System.Reflection.Metadata; using Draco.Compiler.Internal.Symbols.Generic; using Draco.Compiler.Internal.Symbols.Metadata; +using Draco.Compiler.Internal.Utilities; using static Draco.Compiler.Internal.OptimizingIr.InstructionFactory; namespace Draco.Compiler.Internal.Symbols.Synthetized; diff --git a/src/Draco.Compiler/Internal/Symbols/TypeSymbol.cs b/src/Draco.Compiler/Internal/Symbols/TypeSymbol.cs index 368135297..3f18303c1 100644 --- a/src/Draco.Compiler/Internal/Symbols/TypeSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/TypeSymbol.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Linq; using Draco.Compiler.Internal.Symbols.Generic; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols; @@ -73,7 +74,7 @@ public IEnumerable BaseTypes public override bool CanBeShadowedBy(Symbol other) { - if (other is not TypeSymbol type) return false; + if (other is not TypeSymbol) return false; if (this.Name != other.Name) return false; return this.GenericParameters.Length == other.GenericParameters.Length; } diff --git a/src/Draco.Compiler/Internal/Symbols/WellKnownTypes.cs b/src/Draco.Compiler/Internal/Symbols/WellKnownTypes.cs index 30b4d5d41..0a85cd689 100644 --- a/src/Draco.Compiler/Internal/Symbols/WellKnownTypes.cs +++ b/src/Draco.Compiler/Internal/Symbols/WellKnownTypes.cs @@ -6,6 +6,7 @@ using Draco.Compiler.Internal.Symbols.Generic; using Draco.Compiler.Internal.Symbols.Metadata; using Draco.Compiler.Internal.Symbols.Synthetized; +using Draco.Compiler.Internal.Utilities; namespace Draco.Compiler.Internal.Symbols; @@ -33,6 +34,17 @@ internal sealed partial class WellKnownTypes .Single(m => m.Name == "ToString")); private MetadataMethodSymbol? object_ToString; + /// + /// object.Equals(object? o1, object? o2). + /// + public MetadataMethodSymbol SystemObject_Equals => InterlockedUtils.InitializeNull( + ref this.object_Equals, + () => this.SystemObject + .Members + .OfType() + .Single(m => m.Name == "Equals" && m.Parameters.Length == 2)); + private MetadataMethodSymbol? object_Equals; + /// /// string.Format(string formatString, object[] args). /// diff --git a/src/Draco.Compiler/Internal/Syntax/Lexer.cs b/src/Draco.Compiler/Internal/Syntax/Lexer.cs index 97e2a4a53..5522f125a 100644 --- a/src/Draco.Compiler/Internal/Syntax/Lexer.cs +++ b/src/Draco.Compiler/Internal/Syntax/Lexer.cs @@ -216,6 +216,7 @@ Unit TakeWithText(TokenKind tokenKind, int length) return TakeBasic(TokenKind.Plus, 1); case '-': if (this.Peek(1) == '=') return TakeBasic(TokenKind.MinusAssign, 2); + if (this.Peek(1) == '>') return TakeBasic(TokenKind.Arrow, 2); return TakeBasic(TokenKind.Minus, 1); case '*': if (this.Peek(1) == '=') return TakeBasic(TokenKind.StarAssign, 2); @@ -319,6 +320,7 @@ Unit TakeWithText(TokenKind tokenKind, int length) // TODO: Any better/faster way? var tokenKind = ident switch { + var _ when ident.Span.SequenceEqual("_") => TokenKind.KeywordDiscard, var _ when ident.Span.SequenceEqual("and") => TokenKind.KeywordAnd, var _ when ident.Span.SequenceEqual("else") => TokenKind.KeywordElse, var _ when ident.Span.SequenceEqual("false") => TokenKind.KeywordFalse, @@ -329,6 +331,7 @@ var _ when ident.Span.SequenceEqual("if") => TokenKind.KeywordIf, var _ when ident.Span.SequenceEqual("import") => TokenKind.KeywordImport, var _ when ident.Span.SequenceEqual("in") => TokenKind.KeywordIn, var _ when ident.Span.SequenceEqual("internal") => TokenKind.KeywordInternal, + var _ when ident.Span.SequenceEqual("match") => TokenKind.KeywordMatch, var _ when ident.Span.SequenceEqual("mod") => TokenKind.KeywordMod, var _ when ident.Span.SequenceEqual("module") => TokenKind.KeywordModule, var _ when ident.Span.SequenceEqual("not") => TokenKind.KeywordNot, diff --git a/src/Draco.Compiler/Internal/Syntax/Parser.cs b/src/Draco.Compiler/Internal/Syntax/Parser.cs index 9064935b6..8651b905b 100644 --- a/src/Draco.Compiler/Internal/Syntax/Parser.cs +++ b/src/Draco.Compiler/Internal/Syntax/Parser.cs @@ -184,6 +184,7 @@ private ExpressionParserDelegate BinaryRight(params TokenKind[] operators) => le TokenKind.KeywordFunc, TokenKind.KeywordGoto, TokenKind.KeywordIf, + TokenKind.KeywordMatch, TokenKind.KeywordNot, TokenKind.KeywordReturn, TokenKind.KeywordTrue, @@ -313,6 +314,7 @@ internal StatementSyntax ParseStatement(bool allowDecl) case TokenKind.KeywordIf: case TokenKind.KeywordWhile: case TokenKind.KeywordFor: + case TokenKind.KeywordMatch: { var expr = this.ParseControlFlowExpression(ControlFlowContext.Stmt); return new ExpressionStatementSyntax(expr, null); @@ -669,13 +671,15 @@ private ExpressionSyntax ParseControlFlowExpression(ControlFlowContext ctx) Debug.Assert(peekKind is TokenKind.CurlyOpen or TokenKind.KeywordIf or TokenKind.KeywordWhile - or TokenKind.KeywordFor); + or TokenKind.KeywordFor + or TokenKind.KeywordMatch); return peekKind switch { TokenKind.CurlyOpen => this.ParseBlockExpression(ctx), TokenKind.KeywordIf => this.ParseIfExpression(ctx), TokenKind.KeywordWhile => this.ParseWhileExpression(ctx), TokenKind.KeywordFor => this.ParseForExpression(ctx), + TokenKind.KeywordMatch => this.ParseMatchExpression(ctx), _ => throw new InvalidOperationException(), }; } @@ -743,6 +747,7 @@ private ParsedBlock ParseBlock(ControlFlowContext ctx) case TokenKind.KeywordIf: case TokenKind.KeywordWhile: case TokenKind.KeywordFor: + case TokenKind.KeywordMatch: { var expr = this.ParseControlFlowExpression(ctx); if (ctx == ControlFlowContext.Expr && this.Peek() == TokenKind.CurlyClose) @@ -823,6 +828,61 @@ private IfExpressionSyntax ParseIfExpression(ControlFlowContext ctx) return new(ifKeyword, openParen, condition, closeParen, thenBody, elsePart); } + /// + /// Parses an match-expression. + /// + /// The current context we are in. + /// The parsed . + private MatchExpressionSyntax ParseMatchExpression(ControlFlowContext ctx) + { + var matchKeyword = this.Expect(TokenKind.KeywordMatch); + var openParen = this.Expect(TokenKind.ParenOpen); + var matchedValue = this.ParseExpression(); + var closeParen = this.Expect(TokenKind.ParenClose); + var openBrace = this.Expect(TokenKind.CurlyOpen); + + var matchArms = SyntaxList.CreateBuilder(); + SyntaxToken? closeBrace; + while (!this.Matches(TokenKind.CurlyClose, out closeBrace)) + { + matchArms.Add(this.ParseMatchArm(ctx)); + } + + return new(matchKeyword, openParen, matchedValue, closeParen, openBrace, matchArms.ToSyntaxList(), closeBrace); + } + + /// + /// Parses a single match arm. + /// + /// The current context we are in. + /// The parsed . + private MatchArmSyntax ParseMatchArm(ControlFlowContext ctx) + { + var pattern = this.ParsePattern(); + + GuardClauseSyntax? guardClause = null; + if (this.Peek() == TokenKind.KeywordIf) guardClause = this.ParseGuardClause(); + + var arrow = this.Expect(TokenKind.Arrow); + var value = this.ParseExpression(); + var semicolon = this.Expect(TokenKind.Semicolon); + + return new(pattern, guardClause, arrow, value, semicolon); + } + + /// + /// Parses a guard clause. + /// + /// The parsed . + private GuardClauseSyntax ParseGuardClause() + { + var ifKeyword = this.Expect(TokenKind.KeywordIf); + var openParen = this.Expect(TokenKind.ParenOpen); + var condition = this.ParseExpression(); + var closeParen = this.Expect(TokenKind.ParenClose); + return new(ifKeyword, openParen, condition, closeParen); + } + /// /// Parses a while-expression. /// @@ -1029,6 +1089,7 @@ private ExpressionSyntax ParseAtomExpression(int level) case TokenKind.KeywordIf: case TokenKind.KeywordWhile: case TokenKind.KeywordFor: + case TokenKind.KeywordMatch: return this.ParseControlFlowExpression(ControlFlowContext.Expr); default: { @@ -1189,6 +1250,38 @@ private StringExpressionSyntax ParseMultiLineString() return new(openQuote, content.ToSyntaxList(), closeQuote); } + /// + /// Parses a pattern. + /// + /// The parsed . + private PatternSyntax ParsePattern() + { + switch (this.Peek()) + { + case TokenKind.LiteralInteger: + return new LiteralPatternSyntax(this.Advance()); + case TokenKind.KeywordDiscard: + return new DiscardPatternSyntax(this.Advance()); + default: + { + var input = this.Synchronize(t => t switch + { + TokenKind.Semicolon or TokenKind.Comma + or TokenKind.ParenClose or TokenKind.BracketClose + or TokenKind.CurlyClose or TokenKind.InterpolationEnd + or TokenKind.Assign or TokenKind.Arrow => false, + _ when IsExpressionStarter(t) => false, + _ => true, + }); + var info = DiagnosticInfo.Create(SyntaxErrors.UnexpectedInput, formatArgs: "type"); + var diag = new SyntaxDiagnosticInfo(info, Offset: 0, Width: input.FullWidth); + var node = new UnexpectedPatternSyntax(input); + this.AddDiagnostic(node, diag); + return node; + } + } + } + /// /// Checks, if a given syntax can be followed by a generic argument list. /// diff --git a/src/Draco.Compiler/Internal/Syntax/Syntax.xml b/src/Draco.Compiler/Internal/Syntax/Syntax.xml index a34baeb51..3e7a14690 100644 --- a/src/Draco.Compiler/Internal/Syntax/Syntax.xml +++ b/src/Draco.Compiler/Internal/Syntax/Syntax.xml @@ -742,6 +742,127 @@ + + + An match-expression. + + + + + The keyword 'match'. + + + + + + + The open parenthesis before the matched value. + + + + + + + The value we match on. + + + + + The close parenthesis after the matched value. + + + + + + + The opening brace before the match arms. + + + + + + + The arms of this match expression. + + + + + The closing brace after the match arms. + + + + + + + + + A single arm of a match expression that matches a pattern and assigns a value to it. + + + + + The pattern that the arm matches. + + + + + The optional guard clause of the arm. + + + + + The arrow after the pattern and clause. + + + + + + + The value the arm evaluates to. + + + + + The semicolon terminating the arm. + + + + + + + + + A guard clause. + + + + + The keyword 'if'. + + + + + + + The open parenthesis before the condition. + + + + + + + The condition. + + + + + The close parenthesis after the condition. + + + + + + A while-expression. @@ -1275,4 +1396,50 @@ + + + + A pattern. + + + + + + Unexpected input in pattern expression context. + + + + + The unexpected syntax nodes. + + + + + + + A literal pattern. + + + + + The literal token representing the value. + + + + + + + + + A pattern matching anything, discarding the value. + + + + + The underscore signaling the discard. + + + + + diff --git a/src/Draco.Compiler/Internal/UntypedTree/UntypedNode.cs b/src/Draco.Compiler/Internal/UntypedTree/UntypedNode.cs index 028cc5128..0fb2dcae6 100644 --- a/src/Draco.Compiler/Internal/UntypedTree/UntypedNode.cs +++ b/src/Draco.Compiler/Internal/UntypedTree/UntypedNode.cs @@ -106,6 +106,11 @@ internal partial class UntypedAssignmentExpression // Lvalues +internal partial class UntypedLvalue +{ + public abstract TypeSymbol Type { get; } +} + internal partial class UntypedUnexpectedLvalue { public override TypeSymbol Type => IntrinsicSymbols.ErrorType; @@ -126,12 +131,24 @@ internal partial class UntypedPropertySetLvalue public override TypeSymbol Type => this.Setter.Parameters[0].Type; } -internal partial class UntypedLvalue +internal partial class UntypedGlobalLvalue +{ + public override TypeSymbol Type => this.Global.Type; +} + +// Patterns + +internal partial class UntypedPattern { public abstract TypeSymbol Type { get; } } -internal partial class UntypedGlobalLvalue +internal partial class UntypedUnexpectedPattern { - public override TypeSymbol Type => this.Global.Type; + public override TypeSymbol Type => IntrinsicSymbols.Never; +} + +internal partial class UntypedDiscardPattern +{ + public override TypeSymbol Type => IntrinsicSymbols.Never; } diff --git a/src/Draco.Compiler/Internal/UntypedTree/UntypedNodes.xml b/src/Draco.Compiler/Internal/UntypedTree/UntypedNodes.xml index 02f821785..18bec58fd 100644 --- a/src/Draco.Compiler/Internal/UntypedTree/UntypedNodes.xml +++ b/src/Draco.Compiler/Internal/UntypedTree/UntypedNodes.xml @@ -9,6 +9,7 @@ + @@ -66,6 +67,18 @@ + + + + + + + + + + + + @@ -262,4 +275,17 @@ + + + + + + + + + + + + + diff --git a/src/Draco.Compiler/Internal/Utilities/BinarySearch.cs b/src/Draco.Compiler/Internal/Utilities/BinarySearch.cs new file mode 100644 index 000000000..bc07bd995 --- /dev/null +++ b/src/Draco.Compiler/Internal/Utilities/BinarySearch.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace Draco.Compiler.Internal.Utilities; + +/// +/// Generic binary search implementation. +/// +internal static class BinarySearch +{ + /// + /// Performs a binary search. + /// + /// The item type. + /// The searched key type. + /// The span of items to search in. + /// The searched key. + /// The key selector. + /// The key comparer. + /// A tuple of the index that tells where the item can be inserted to maintain ordered-ness + /// and a boolean indicating if it was an exact match. + public static (int Index, bool ExactMatch) Search( + ReadOnlySpan items, + TKey searchedKey, + Func keySelector, + IComparer? keyComparer = null) + { + keyComparer ??= Comparer.Default; + + var size = items.Length; + var left = 0; + var right = size; + + while (left < right) + { + var mid = left + size / 2; + var cmp = keyComparer.Compare(searchedKey, keySelector(items[mid])); + + if (cmp == 0) return (mid, true); + + if (cmp > 0) left = mid + 1; + else right = mid; + + size = right - left; + } + + return (left, false); + } +} diff --git a/src/Draco.Compiler/Internal/Utilities/DictionaryEqualityComparer.cs b/src/Draco.Compiler/Internal/Utilities/DictionaryEqualityComparer.cs deleted file mode 100644 index 7a3d90680..000000000 --- a/src/Draco.Compiler/Internal/Utilities/DictionaryEqualityComparer.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Draco.Compiler.Internal.Utilities; - -/// -/// A comparer for dictionaries that compares contents. -/// -/// The key type. -/// The value type. -internal sealed class DictionaryEqualityComparer : - IEqualityComparer>, - IEqualityComparer> -{ - /// - /// A default equality comparer. - /// - public static DictionaryEqualityComparer Default { get; } = - new(EqualityComparer.Default, EqualityComparer.Default); - - /// - /// The comparer for keys. - /// - public IEqualityComparer KeyComparer { get; } - - /// - /// The comparer for values. - /// - public IEqualityComparer ValueComparer { get; } - - public DictionaryEqualityComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer) - { - this.KeyComparer = keyComparer; - this.ValueComparer = valueComparer; - } - - public bool Equals(IReadOnlyDictionary? x, IReadOnlyDictionary? y) - { - if (ReferenceEquals(x, y)) return true; - if (x is null || y is null) return false; - if (x.Count != y.Count) return false; - foreach (var (key, v1) in x) - { - if (!y.TryGetValue(key, out var v2)) return false; - if (!this.ValueComparer.Equals(v1, v2)) return false; - } - return true; - } - public bool Equals(IDictionary? x, IDictionary? y) - { - if (ReferenceEquals(x, y)) return true; - if (x is null || y is null) return false; - if (x.Count != y.Count) return false; - foreach (var (key, v1) in x) - { - if (!y.TryGetValue(key, out var v2)) return false; - if (!this.ValueComparer.Equals(v1, v2)) return false; - } - return true; - } - - public int GetHashCode([DisallowNull] IReadOnlyDictionary obj) - { - // We use XOR for order-independence - var h = 0; - foreach (var kv in obj) h ^= this.GetHashCode(kv); - return h; - } - public int GetHashCode([DisallowNull] IDictionary obj) - { - // We use XOR for order-independence - var h = 0; - foreach (var kv in obj) h ^= this.GetHashCode(kv); - return h; - } - - private int GetHashCode(KeyValuePair obj) => - this.KeyComparer.GetHashCode(obj.Key!) * 31 + this.ValueComparer.GetHashCode(obj.Value!); -} diff --git a/src/Draco.Compiler/Internal/Utilities/EnumerableExtensions.cs b/src/Draco.Compiler/Internal/Utilities/EnumerableExtensions.cs deleted file mode 100644 index 854507bce..000000000 --- a/src/Draco.Compiler/Internal/Utilities/EnumerableExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; - -namespace Draco.Compiler.Internal.Utilities; - -/// -/// Extensions for s. -/// -internal static partial class EnumerableExtensions -{ - /// - /// Checks if a given sequence is ordered in ascending order. - /// - /// The item type. - /// The sequence to check. - /// The comparer to compare items with. - /// True, if are ordered in ascending order. - public static bool IsOrdered(this IEnumerable vs, IComparer? comparer = null) - { - comparer ??= Comparer.Default; - var enumerator = vs.GetEnumerator(); - // Empty sequences are considered sorted - if (!enumerator.MoveNext()) return true; - var prev = enumerator.Current; - while (enumerator.MoveNext()) - { - var curr = enumerator.Current; - if (comparer.Compare(prev, curr) > 0) return false; - prev = curr; - } - return true; - } -} diff --git a/src/Draco.Compiler/Internal/InterlockedUtils.cs b/src/Draco.Compiler/Internal/Utilities/InterlockedUtils.cs similarity index 95% rename from src/Draco.Compiler/Internal/InterlockedUtils.cs rename to src/Draco.Compiler/Internal/Utilities/InterlockedUtils.cs index 3a664d974..fecfb2f54 100644 --- a/src/Draco.Compiler/Internal/InterlockedUtils.cs +++ b/src/Draco.Compiler/Internal/Utilities/InterlockedUtils.cs @@ -2,7 +2,7 @@ using System.Collections.Immutable; using System.Threading; -namespace Draco.Compiler.Internal; +namespace Draco.Compiler.Internal.Utilities; /// /// Utility functions for atomic operations. diff --git a/src/Draco.Compiler/Internal/Utilities/Option.cs b/src/Draco.Compiler/Internal/Utilities/Option.cs deleted file mode 100644 index 27ca2b026..000000000 --- a/src/Draco.Compiler/Internal/Utilities/Option.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Draco.Compiler.Internal.Utilities; - -/// -/// A type representing an optional (nullable) value, that works for both reference and value types. -/// -/// The type of the optional value. -internal readonly struct Option : IEquatable>, IEquatable -{ - /// - /// A none value. - /// - public static readonly Option None = new(); - - /// - /// The unwrapped value, in case it's not none. - /// - /// - public T? Value => this.IsSome - ? this.value - : throw new InvalidOperationException("Option was None"); - - /// - /// True, if the option contains a value. - /// - [MemberNotNullWhen(true, nameof(Value))] - public bool IsSome { get; } - - /// - /// True, if the option is none. - /// - [MemberNotNullWhen(false, nameof(Value))] - public bool IsNone => !this.IsSome; - - private readonly T value; - - public Option() - { - this.IsSome = false; - this.value = default!; - } - - public Option(T value) - { - this.IsSome = true; - this.value = value; - } - - public override bool Equals([NotNullWhen(true)] object? obj) => - obj is Option other - && this.Equals(other); - public bool Equals(Option other) - { - if (this.IsNone) return other.IsNone; - if (other.IsNone) return false; - return this.Value.Equals(other.Value); - } - public bool Equals(T? other) => this.IsSome && this.Value.Equals(other); - public override int GetHashCode() => this.IsSome - ? this.Value.GetHashCode() - : 0; - - /// - /// Maps the value of the option to another type. - /// - /// The type of the new value. - /// A transformation function mapping the value. - /// A new option of either the new transformed value or none. - public Option Map(Func transform) => this.IsSome - ? new(transform(this.Value)) - : Option.None; - - /// - /// Binds the value to a new option with another value type. - /// - /// The type of the new value. - /// A transformation function mapping the value to a new option. - /// A new option of either the returned option or none. - public Option BindOk(Func> transform) => this.IsSome - ? transform(this.Value) - : Option.None; - - /// - /// Matches the value or none. - /// - /// The type of the resulting value. - /// The function to apply if the result is some. - /// The function to apply if the result is none. - /// The result of matching either the value or none. - public TResult Match(Func ifSome, Func ifNone) => this.IsSome - ? ifSome(this.Value) - : ifNone(); - - /// - /// Switches on the value or none. - /// - /// The function to apply if there is a value. - /// The function to apply if the value is none. - public void Switch(Action ifSome, Action isNone) - { - if (this.IsSome) - { - ifSome(this.Value); - } - else - { - isNone(); - } - } - - /// - /// Returns the value or a value from a factory function. - /// - /// The factory function to invoke if the value is none. - public T ValueOr(Func factory) => this.IsSome - ? this.Value - : factory(); - - /// - /// Returns the value or a default value. - /// - /// The value to return if the value is none. - public T ValueOr(T defaultValue) => - this.ValueOr(() => defaultValue); - - /// - /// Implicitly converts a value to an . - /// - /// The value to convert. - public static implicit operator Option(T value) => - new(value); - - public static bool operator ==(Option o1, Option o2) => o1.Equals(o2); - public static bool operator !=(Option o1, Option o2) => !o1.Equals(o2); - public static bool operator ==(Option o1, T o2) => o1.Equals(o2); - public static bool operator !=(Option o1, T o2) => !o1.Equals(o2); - public static bool operator ==(T o1, Option o2) => o2.Equals(o1); - public static bool operator !=(T o1, Option o2) => !o2.Equals(o1); -} - -/// -/// Extension functionality for . -/// -internal static class Option -{ - /// - /// Constructs an containing the given value. - /// - /// The type of the optional value. - /// The stored value. - /// The wrapped up in . - public static Option Some(T value) => new(value); - - /// - /// Constructs a none . - /// - /// The type of the optional value. - /// An representing no value. - public static Option None() => Option.None; - - /// - /// Constructs an from a nullable reference. - /// - /// The reference type. - /// The reference to construct from. - /// The optional representation of . - public static Option FromNullableReference(T? value) - where T : class => value is null - ? Option.None - : new(value); - - /// - /// Constructs an from a nullable value. - /// - /// The value type. - /// The nullable value to construct from. - /// The optional representation of . - public static Option FromNullableValue(T? value) - where T : struct => value is null - ? Option.None - : new(value.Value); - - /// - /// Converts an to a nullable reference. - /// - /// The reference type. - /// The to convert. - /// The value of , or null. - public static T? ToNullableReference(this Option option) - where T : class => option.IsSome - ? option.Value - : null; - - /// - /// Converts an to a nullable value. - /// - /// The value type. - /// The to convert. - /// The value of , or null. - public static T? ToNullableValue(this Option option) - where T : struct => option.IsSome - ? option.Value - : null; -} diff --git a/src/Draco.Compiler/Internal/Utilities/Result.cs b/src/Draco.Compiler/Internal/Utilities/Result.cs deleted file mode 100644 index c64c63ac5..000000000 --- a/src/Draco.Compiler/Internal/Utilities/Result.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Draco.Compiler.Internal.Utilities; - -/// -/// A union type containing either the result of an operation or an error. -/// -/// The type of the inner ok value. -/// The type of the inner error. -internal readonly struct Result -{ - /// - /// The value of the result. Throws an exception if the result is not an ok value. - /// - /// - public TOk? Ok => this.IsOk - ? this.ok - : throw new InvalidOperationException("Result is not an ok value."); - - /// - /// The error of the result. Throws an exception if the result is not an error. - /// - /// - public TError? Error => this.IsOk - ? throw new InvalidOperationException("Result is not an error.") - : this.error; - - /// - /// Whether the result is an ok value or not. - /// - [MemberNotNullWhen(true, nameof(Ok))] - [MemberNotNullWhen(false, nameof(Error))] - public bool IsOk { get; } - - private readonly TOk ok; - private readonly TError error; - - /// - /// Initializes a new with an ok value. - /// - /// The ok value. - public Result(TOk ok) - { - this.ok = ok; - this.error = default!; - this.IsOk = true; - } - - /// - /// Initializes a new with an error value. - /// - /// The error value. - public Result(TError error) - { - this.ok = default!; - this.error = error; - this.IsOk = false; - } - - /// - /// Maps the ok value of the result to another type. - /// - /// The type of the new ok value. - /// A transformation function mapping the ok value. - /// A new result of either the new transformed ok value or the previous error. - public Result MapOk(Func transform) => this.IsOk - ? new(transform(this.ok)) - : new(this.error); - - /// - /// Maps the inner error value of the result to another error type. - /// - /// The type of the new error value. - /// A transformation function mapping the error value. - /// A new result of either the new transformed error value or the previous ok value. - public Result MapError(Func transform) => this.IsOk - ? new(this.ok) - : new(transform(this.error)); - - /// - /// Binds the ok value to a new result with another ok value type. - /// - /// The type of the new ok value. - /// A transformation function mapping the ok value to a new result. - /// A new result of either the returned result or the previous error. - public Result BindOk(Func> transform) => this.IsOk - ? transform(this.ok) - : new(this.error); - - /// - /// Binds the error value to a new result with another error type. - /// - /// The type of the new error value. - /// A transformation function mapping the error value to a new result. - /// A new result of either the returned result or the previous ok value. - public Result BindError(Func> transform) => this.IsOk - ? new(this.ok) - : transform(this.error); - - /// - /// Matches the ok value or error. - /// - /// The type of the resulting value. - /// The function to apply if the result is an ok value. - /// The function to apply if the result is an error value. - /// The result of matching either the ok value or the error value. - public TResult Match(Func ifOk, Func ifError) => this.IsOk - ? ifOk(this.ok) - : ifError(this.error); - - /// - /// Switches on the ok value or error. - /// - /// The function to apply if the result is an ok value. - /// The function to apply if the result is an error value. - public void Switch(Action ifOk, Action ifError) - { - if (this.IsOk) - { - ifOk(this.ok); - } - else - { - ifError(this.error); - } - } - - /// - /// Returns the ok value or a value from a factory function. - /// - /// The factory function to invoke if the result is not an ok value. - public TOk OkOr(Func factory) => this.IsOk - ? this.ok - : factory(); - - /// - /// Returns the ok value or a default value. - /// - /// The value to return if the result is not an ok value. - public TOk OkOr(TOk defaultValue) => - this.OkOr(() => defaultValue); - - /// - /// Returns the error value or a value from a factory function. - /// - /// The factory function to invoke if the result is not an error value. - public TError ErrorOr(Func factory) => this.IsOk - ? factory() - : this.error; - - /// - /// Returns the error value or a default value. - /// - /// The value to return if the result is not an error value. - public TError ErrorOr(TError defaultValue) => - this.ErrorOr(() => defaultValue); - - /// - /// Implicitly converts an ok value to a . - /// - /// The ok value to convert. - public static implicit operator Result(TOk ok) => - new(ok); - - /// - /// Implicitly converts an error value to a . - /// - /// The error value to convert. - public static implicit operator Result(TError error) => - new(error); -} diff --git a/src/Draco.SourceGeneration/Templates/BoundTree.sbncs b/src/Draco.SourceGeneration/Templates/BoundTree.sbncs index 9c6058773..1fb6b3edb 100644 --- a/src/Draco.SourceGeneration/Templates/BoundTree.sbncs +++ b/src/Draco.SourceGeneration/Templates/BoundTree.sbncs @@ -83,7 +83,7 @@ namespace Draco.Compiler.Internal.BoundTree; public override string ToString() { var result = new StringBuilder(); - result.Append("{{$node.Name}}"); + result.Append("{{remove_prefix($node.Name, 'Bound')}}"); result.Append('('); {{has_more_fields = array.size($node.Fields) > 1}} diff --git a/src/Draco.SourceGeneration/Templates/RedSyntaxTree.sbncs b/src/Draco.SourceGeneration/Templates/RedSyntaxTree.sbncs index b1c18ab6a..0e28a211e 100644 --- a/src/Draco.SourceGeneration/Templates/RedSyntaxTree.sbncs +++ b/src/Draco.SourceGeneration/Templates/RedSyntaxTree.sbncs @@ -25,9 +25,9 @@ namespace Draco.Compiler.Api.Syntax; {{else}} {{field_prefix($field)}} => {{if $field.IsNullable}} - Internal.InterlockedUtils.InitializeMaybeNull( + Internal.Utilities.InterlockedUtils.InitializeMaybeNull( {{else}} - Internal.InterlockedUtils.InitializeNull( + Internal.Utilities.InterlockedUtils.InitializeNull( {{end}} ref this.{{$backingField}}, () => ({{$field.Type}})this.Green.{{$field.Name}}{{nullable($field)}}.ToRedNode(this.Tree, this, {{$fieldFullPosition}})); diff --git a/src/Draco.SourceGeneration/Templates/WellKnownTypes.sbncs b/src/Draco.SourceGeneration/Templates/WellKnownTypes.sbncs index e96012242..73ad7b8aa 100644 --- a/src/Draco.SourceGeneration/Templates/WellKnownTypes.sbncs +++ b/src/Draco.SourceGeneration/Templates/WellKnownTypes.sbncs @@ -24,7 +24,7 @@ internal sealed partial class WellKnownTypes {{for $b in $assembly.PublicKeyToken}}{{$b}}, {{end}} }; - public MetadataAssemblySymbol {{$prop_name}} => InterlockedUtils.InitializeNull( + public MetadataAssemblySymbol {{$prop_name}} => Internal.Utilities.InterlockedUtils.InitializeNull( ref this.{{$field_name}}, () => this.GetAssemblyWithNameAndToken("{{$assembly.Name}}", {{$prop_name}}_PublicKeyToken)); private MetadataAssemblySymbol? {{$field_name}}; @@ -35,7 +35,7 @@ internal sealed partial class WellKnownTypes {{$field_name = camel_case($prop_name)}} {{$path = string.split($type.Name, '.')}} - public MetadataTypeSymbol {{$prop_name}} => InterlockedUtils.InitializeNull( + public MetadataTypeSymbol {{$prop_name}} => Internal.Utilities.InterlockedUtils.InitializeNull( ref this.{{$field_name}}, () => this.GetTypeFromAssembly( {{nameify($type.Assembly.Name)}}, diff --git a/src/Draco.SyntaxHighlighting/draco.tmLanguage.json b/src/Draco.SyntaxHighlighting/draco.tmLanguage.json index 4e493e0f0..34349412c 100644 --- a/src/Draco.SyntaxHighlighting/draco.tmLanguage.json +++ b/src/Draco.SyntaxHighlighting/draco.tmLanguage.json @@ -17,6 +17,9 @@ }, { "include": "#expressions" + }, + { + "include": "#patterns" } ] }, @@ -277,7 +280,7 @@ }, { "name": "keyword.control.draco", - "match": "\\b(if|else|while|for|in|return|goto)\\b" + "match": "\\b(if|else|while|for|in|return|goto|match)\\b" }, { "include": "#call-expression" @@ -294,6 +297,17 @@ } ] }, + "patterns": { + "patterns": [ + { + "include": "#comments" + }, + { + "name": "variable.language.discard.draco", + "match": "\\b_\\b" + } + ] + }, "call-expression": { "begin": "\\b([[:alpha:]_][[:alnum:]_]*)\\b\\s*(?\\s*<([^<>]|\\g)+>\\s*)?\\s*(?=[(])", "beginCaptures": { diff --git a/src/Draco.SyntaxHighlighting/draco.tmLanguage.yml b/src/Draco.SyntaxHighlighting/draco.tmLanguage.yml index 3d15abe80..b2d7fd165 100644 --- a/src/Draco.SyntaxHighlighting/draco.tmLanguage.yml +++ b/src/Draco.SyntaxHighlighting/draco.tmLanguage.yml @@ -8,6 +8,7 @@ repository: - include: '#comments' - include: '#statements' - include: '#expressions' + - include: '#patterns' comments: patterns: - name: comment.line.double-slash.draco @@ -139,13 +140,19 @@ repository: match: '[><=!]=?' # Control keywords that build up expressions - name: keyword.control.draco - match: \b(if|else|while|for|in|return|goto)\b + match: \b(if|else|while|for|in|return|goto|match)\b - include: '#call-expression' # Variable name - name: variable.other.draco match: '[[:alpha:]_][[:alnum:]_]*' - include: '#block' - include: '#literals' + patterns: + patterns: + - include: '#comments' + # Discard pattern + - name: variable.language.discard.draco + match: \b_\b call-expression: # Function name, generic args, unconsumed open paren begin: \b([[:alpha:]_][[:alnum:]_]*)\b\s*(?\s*<([^<>]|\g)+>\s*)?\s*(?=[(])