├── .vscode ├── extensions.json └── launch.json ├── samples ├── hello │ ├── hello.ms │ └── hello.msproj ├── Directory.Build.props ├── samples.sln └── Directory.Build.targets ├── msi.cmd ├── msc.cmd ├── msi.sh ├── msc.sh ├── src ├── msi │ ├── Program.cs │ ├── Authoring │ │ ├── Classification.cs │ │ ├── ClassifiedSpan.cs │ │ └── Classifier.cs │ └── msi.csproj ├── Minsk │ ├── CodeAnalysis │ │ ├── Symbols │ │ │ ├── SymbolKind.cs │ │ │ ├── ParameterSymbol.cs │ │ │ ├── LocalVariableSymbol.cs │ │ │ ├── GlobalVariableSymbol.cs │ │ │ ├── VariableSymbol.cs │ │ │ ├── Symbol.cs │ │ │ ├── TypeSymbol.cs │ │ │ ├── FunctionSymbol.cs │ │ │ ├── BuiltinFunctions.cs │ │ │ └── SymbolPrinter.cs │ │ ├── Binding │ │ │ ├── BoundUnaryOperatorKind.cs │ │ │ ├── BoundConstant.cs │ │ │ ├── BoundStatement.cs │ │ │ ├── BoundLabel.cs │ │ │ ├── BoundNopStatement.cs │ │ │ ├── BoundExpression.cs │ │ │ ├── BoundGotoStatement.cs │ │ │ ├── BoundLabelStatement.cs │ │ │ ├── BoundBinaryOperatorKind.cs │ │ │ ├── BoundReturnStatement.cs │ │ │ ├── BoundExpressionStatement.cs │ │ │ ├── BoundLoopStatement.cs │ │ │ ├── BoundBlockStatement.cs │ │ │ ├── BoundErrorExpression.cs │ │ │ ├── BoundNode.cs │ │ │ ├── BoundConversionExpression.cs │ │ │ ├── BoundVariableDeclaration.cs │ │ │ ├── BoundWhileStatement.cs │ │ │ ├── BoundDoWhileStatement.cs │ │ │ ├── BoundVariableExpression.cs │ │ │ ├── BoundConditionalGotoStatement.cs │ │ │ ├── BoundIfStatement.cs │ │ │ ├── BoundAssignmentExpression.cs │ │ │ ├── BoundCallExpression.cs │ │ │ ├── BoundNodeKind.cs │ │ │ ├── BoundUnaryExpression.cs │ │ │ ├── BoundCompoundAssignmentExpression.cs │ │ │ ├── BoundBinaryExpression.cs │ │ │ ├── BoundForStatement.cs │ │ │ ├── BoundLiteralExpression.cs │ │ │ ├── BoundProgram.cs │ │ │ ├── BoundGlobalScope.cs │ │ │ ├── BoundUnaryOperator.cs │ │ │ ├── BoundScope.cs │ │ │ ├── Conversion.cs │ │ │ ├── BoundBinaryOperator.cs │ │ │ ├── ConstantFolding.cs │ │ │ └── BoundNodeFactory.cs │ │ ├── Syntax │ │ │ ├── MemberSyntax.cs │ │ │ ├── StatementSyntax.cs │ │ │ ├── ExpressionSyntax.cs │ │ │ ├── BreakStatementSyntax.cs │ │ │ ├── ContinueStatementSyntax.cs │ │ │ ├── GlobalStatementSyntax.cs │ │ │ ├── NameExpressionSyntax.cs │ │ │ ├── ExpressionStatementSyntax.cs │ │ │ ├── ParameterSyntax.cs │ │ │ ├── TypeClauseSyntax.cs │ │ │ ├── ElseClauseSyntax.cs │ │ │ ├── UnaryExpressionSyntax.cs │ │ │ ├── ReturnStatementSyntax.cs │ │ │ ├── CompilationUnitSyntax.cs │ │ │ ├── SyntaxTrivia.cs │ │ │ ├── BinaryExpressionSyntax.cs │ │ │ ├── WhileStatementSyntax.cs │ │ │ ├── AssignmentExpressionSyntax.cs │ │ │ ├── LiteralExpressionSyntax.cs │ │ │ ├── DoWhileStatementSyntax.cs │ │ │ ├── BlockStatementSyntax.cs │ │ │ ├── ParenthesizedExpressionSyntax.cs │ │ │ ├── IfStatementSyntax.cs │ │ │ ├── CallExpressionSyntax.cs │ │ │ ├── VariableDeclarationSyntax.cs │ │ │ ├── ForStatementSyntax.cs │ │ │ ├── FunctionDeclarationSyntax.cs │ │ │ ├── SeparatedSyntaxList.cs │ │ │ ├── SyntaxToken.cs │ │ │ ├── SyntaxKind.cs │ │ │ ├── SyntaxTree.cs │ │ │ └── SyntaxNode.cs │ │ ├── EvaluationResult.cs │ │ ├── DiagnosticExtensions.cs │ │ ├── Text │ │ │ ├── TextLocation.cs │ │ │ ├── TextSpan.cs │ │ │ ├── TextLine.cs │ │ │ └── SourceText.cs │ │ ├── Diagnostic.cs │ │ └── Compilation.cs │ ├── Minsk.csproj │ └── IO │ │ └── TextWriterExtensions.cs ├── Minsk.Generators │ ├── Minsk.Generators.csproj │ └── CurlyIndenter.cs ├── Directory.Build.targets ├── .editorconfig ├── msc │ ├── msc.csproj │ └── Program.cs ├── Minsk.Tests │ ├── CodeAnalysis │ │ ├── Text │ │ │ └── SourceTextTests.cs │ │ ├── Syntax │ │ │ ├── SyntaxFactTests.cs │ │ │ └── AssertingEnumerator.cs │ │ └── AnnotatedText.cs │ └── Minsk.Tests.csproj ├── Directory.Build.props └── minsk.sln ├── genindex.ps1 ├── .editorconfig ├── LICENSE ├── docs ├── episode-09.md ├── episode-26.md ├── episode-01.md ├── episode-05.md ├── episode-03.md ├── episode-02.md ├── episode-19.md ├── episode-13.md ├── episode-17.md ├── episode-23.md ├── episode-14.md ├── episode-06.md ├── episode-15.md ├── episode-11.md ├── episode-27.md └── episode-25.md ├── azure-pipelines.yml ├── azure-pipelines-index.yml ├── README.md └── .gitattributes /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp" 4 | ] 5 | } -------------------------------------------------------------------------------- /samples/hello/hello.ms: -------------------------------------------------------------------------------- 1 | function main() 2 | { 3 | print("What's your name?") 4 | let name = input() 5 | print("Hello " + name + "!") 6 | } 7 | -------------------------------------------------------------------------------- /msi.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Vars 4 | set "SLNDIR=%~dp0src" 5 | 6 | REM Restore + Build 7 | dotnet build "%SLNDIR%\msi" --nologo || exit /b 8 | 9 | REM Run 10 | dotnet run -p "%SLNDIR%\msi" --no-build 11 | -------------------------------------------------------------------------------- /samples/hello/hello.msproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | -------------------------------------------------------------------------------- /msc.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Vars 4 | set "SLNDIR=%~dp0src" 5 | 6 | REM Restore + Build 7 | dotnet build "%SLNDIR%\msc" --nologo || exit /b 8 | 9 | REM Run 10 | dotnet run -p "%SLNDIR%\msc" --no-build -- %* 11 | -------------------------------------------------------------------------------- /msi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Vars 4 | slndir="$(dirname "${BASH_SOURCE[0]}")/src" 5 | 6 | # Restore + Build 7 | dotnet build "$slndir/msi" --nologo || exit 8 | 9 | # Run 10 | dotnet run -p "$slndir/msi" --no-build 11 | -------------------------------------------------------------------------------- /msc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Vars 4 | slndir="$(dirname "${BASH_SOURCE[0]}")/src" 5 | 6 | # Restore + Build 7 | dotnet build "$slndir/msc" --nologo || exit 8 | 9 | # Run 10 | dotnet run -p "$slndir/msc" --no-build -- "$@" 11 | -------------------------------------------------------------------------------- /src/msi/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk 2 | { 3 | internal static class Program 4 | { 5 | private static void Main() 6 | { 7 | var repl = new MinskRepl(); 8 | repl.Run(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/SymbolKind.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Symbols 2 | { 3 | public enum SymbolKind 4 | { 5 | Function, 6 | GlobalVariable, 7 | LocalVariable, 8 | Parameter, 9 | Type, 10 | } 11 | } -------------------------------------------------------------------------------- /src/msi/Authoring/Classification.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Authoring 2 | { 3 | public enum Classification 4 | { 5 | Text, 6 | Keyword, 7 | Identifier, 8 | Number, 9 | String, 10 | Comment 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundUnaryOperatorKind.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Binding 2 | { 3 | internal enum BoundUnaryOperatorKind 4 | { 5 | Identity, 6 | Negation, 7 | LogicalNegation, 8 | OnesComplement 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/MemberSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public abstract class MemberSyntax : SyntaxNode 4 | { 5 | private protected MemberSyntax(SyntaxTree syntaxTree) 6 | : base(syntaxTree) 7 | { 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/StatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public abstract class StatementSyntax : SyntaxNode 4 | { 5 | private protected StatementSyntax(SyntaxTree syntaxTree) 6 | : base(syntaxTree) 7 | { 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public abstract class ExpressionSyntax : SyntaxNode 4 | { 5 | private protected ExpressionSyntax(SyntaxTree syntaxTree) 6 | : base(syntaxTree) 7 | { 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundConstant.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Binding 2 | { 3 | internal sealed class BoundConstant 4 | { 5 | public BoundConstant(object value) 6 | { 7 | Value = value; 8 | } 9 | 10 | public object Value { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal abstract class BoundStatement : BoundNode 6 | { 7 | protected BoundStatement(SyntaxNode syntax) 8 | : base(syntax) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundLabel.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Binding 2 | { 3 | internal sealed class BoundLabel 4 | { 5 | internal BoundLabel(string name) 6 | { 7 | Name = name; 8 | } 9 | 10 | public string Name { get; } 11 | 12 | public override string ToString() => Name; 13 | } 14 | } -------------------------------------------------------------------------------- /genindex.ps1: -------------------------------------------------------------------------------- 1 | $indexDir = "bin\index" 2 | $toolsDir = "bin\indexTools" 3 | $zipFile = "$toolsDir\indexTools.zip" 4 | $url = "https://www.nuget.org/api/v2/package/SourceBrowser/1.0.25" 5 | $toolExe = "$toolsDir\tools\HtmlGenerator.exe" 6 | $sln = "src\minsk.sln" 7 | 8 | mkdir $toolsDir | out-null 9 | wget $url -outFile $zipFile | out-null 10 | Expand-Archive $zipFile $toolsDir | out-null 11 | & $toolExe $sln /out:$indexDir 12 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundNopStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundNopStatement : BoundStatement 6 | { 7 | public BoundNopStatement(SyntaxNode syntax) 8 | : base(syntax) 9 | { 10 | } 11 | 12 | public override BoundNodeKind Kind => BoundNodeKind.NopStatement; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Minsk.Generators/Minsk.Generators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | netstandard2.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/msi/msi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Exe 6 | false 7 | Minsk 8 | Minsk 9 | netcoreapp3.1 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/msi/Authoring/ClassifiedSpan.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Text; 2 | 3 | namespace Minsk.CodeAnalysis.Authoring 4 | { 5 | public sealed class ClassifiedSpan 6 | { 7 | public ClassifiedSpan(TextSpan span, Classification classification) 8 | { 9 | Span = span; 10 | Classification = classification; 11 | } 12 | 13 | public TextSpan Span { get; } 14 | public Classification Classification { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/ParameterSymbol.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Symbols 2 | { 3 | public sealed class ParameterSymbol : LocalVariableSymbol 4 | { 5 | internal ParameterSymbol(string name, TypeSymbol type, int ordinal) 6 | : base(name, isReadOnly: true, type, null) 7 | { 8 | Ordinal = ordinal; 9 | } 10 | 11 | public override SymbolKind Kind => SymbolKind.Parameter; 12 | public int Ordinal { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/LocalVariableSymbol.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Binding; 2 | 3 | namespace Minsk.CodeAnalysis.Symbols 4 | { 5 | public class LocalVariableSymbol : VariableSymbol 6 | { 7 | internal LocalVariableSymbol(string name, bool isReadOnly, TypeSymbol type, BoundConstant? constant) 8 | : base(name, isReadOnly, type, constant) 9 | { 10 | } 11 | 12 | public override SymbolKind Kind => SymbolKind.LocalVariable; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/GlobalVariableSymbol.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Binding; 2 | 3 | namespace Minsk.CodeAnalysis.Symbols 4 | { 5 | public sealed class GlobalVariableSymbol : VariableSymbol 6 | { 7 | internal GlobalVariableSymbol(string name, bool isReadOnly, TypeSymbol type, BoundConstant? constant) 8 | : base(name, isReadOnly, type, constant) 9 | { 10 | } 11 | 12 | public override SymbolKind Kind => SymbolKind.GlobalVariable; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/BreakStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | internal sealed partial class BreakStatementSyntax : StatementSyntax 4 | { 5 | internal BreakStatementSyntax(SyntaxTree syntaxTree, SyntaxToken keyword) 6 | : base(syntaxTree) 7 | { 8 | Keyword = keyword; 9 | } 10 | 11 | public override SyntaxKind Kind => SyntaxKind.BreakStatement; 12 | public SyntaxToken Keyword { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal abstract class BoundExpression : BoundNode 8 | { 9 | protected BoundExpression(SyntaxNode syntax) 10 | : base(syntax) 11 | { 12 | } 13 | 14 | public abstract TypeSymbol Type { get; } 15 | public virtual BoundConstant? ConstantValue => null; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ContinueStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | internal sealed partial class ContinueStatementSyntax : StatementSyntax 4 | { 5 | internal ContinueStatementSyntax(SyntaxTree syntaxTree, SyntaxToken keyword) 6 | : base(syntaxTree) 7 | { 8 | Keyword = keyword; 9 | } 10 | 11 | public override SyntaxKind Kind => SyntaxKind.ContinueStatement; 12 | public SyntaxToken Keyword { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/GlobalStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class GlobalStatementSyntax : MemberSyntax 4 | { 5 | internal GlobalStatementSyntax(SyntaxTree syntaxTree, StatementSyntax statement) 6 | : base(syntaxTree) 7 | { 8 | Statement = statement; 9 | } 10 | 11 | public override SyntaxKind Kind => SyntaxKind.GlobalStatement; 12 | public StatementSyntax Statement { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/EvaluationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | 4 | namespace Minsk.CodeAnalysis 5 | { 6 | public sealed class EvaluationResult 7 | { 8 | public EvaluationResult(ImmutableArray diagnostics, object? value) 9 | { 10 | Diagnostics = diagnostics; 11 | Value = value; 12 | } 13 | 14 | public ImmutableArray Diagnostics { get; } 15 | public object? Value { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundGotoStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundGotoStatement : BoundStatement 6 | { 7 | public BoundGotoStatement(SyntaxNode syntax, BoundLabel label) 8 | : base(syntax) 9 | { 10 | Label = label; 11 | } 12 | 13 | public override BoundNodeKind Kind => BoundNodeKind.GotoStatement; 14 | public BoundLabel Label { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundLabelStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundLabelStatement : BoundStatement 6 | { 7 | public BoundLabelStatement(SyntaxNode syntax, BoundLabel label) 8 | : base(syntax) 9 | { 10 | Label = label; 11 | } 12 | 13 | public override BoundNodeKind Kind => BoundNodeKind.LabelStatement; 14 | public BoundLabel Label { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundBinaryOperatorKind.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Binding 2 | { 3 | internal enum BoundBinaryOperatorKind 4 | { 5 | Addition, 6 | Subtraction, 7 | Multiplication, 8 | Division, 9 | LogicalAnd, 10 | LogicalOr, 11 | BitwiseAnd, 12 | BitwiseOr, 13 | BitwiseXor, 14 | Equals, 15 | NotEquals, 16 | Less, 17 | LessOrEquals, 18 | Greater, 19 | GreaterOrEquals, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/NameExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class NameExpressionSyntax : ExpressionSyntax 4 | { 5 | internal NameExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken identifierToken) 6 | : base(syntaxTree) 7 | { 8 | IdentifierToken = identifierToken; 9 | } 10 | 11 | public override SyntaxKind Kind => SyntaxKind.NameExpression; 12 | public SyntaxToken IdentifierToken { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ExpressionStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class ExpressionStatementSyntax : StatementSyntax 4 | { 5 | internal ExpressionStatementSyntax(SyntaxTree syntaxTree, ExpressionSyntax expression) 6 | : base(syntaxTree) 7 | { 8 | Expression = expression; 9 | } 10 | 11 | public override SyntaxKind Kind => SyntaxKind.ExpressionStatement; 12 | public ExpressionSyntax Expression { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .ms 5 | 6 | 7 | 11 | 12 | false 13 | false 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundReturnStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundReturnStatement : BoundStatement 6 | { 7 | public BoundReturnStatement(SyntaxNode syntax, BoundExpression? expression) 8 | : base(syntax) 9 | { 10 | Expression = expression; 11 | } 12 | 13 | public override BoundNodeKind Kind => BoundNodeKind.ReturnStatement; 14 | public BoundExpression? Expression { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundExpressionStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundExpressionStatement : BoundStatement 6 | { 7 | public BoundExpressionStatement(SyntaxNode syntax, BoundExpression expression) 8 | : base(syntax) 9 | { 10 | Expression = expression; 11 | } 12 | 13 | public override BoundNodeKind Kind => BoundNodeKind.ExpressionStatement; 14 | public BoundExpression Expression { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | dotnet_naming_rule.private_fields_start_with_underscore.symbols = private_fields 4 | dotnet_naming_rule.private_fields_start_with_underscore.style = starts_with_underscore 5 | dotnet_naming_rule.private_fields_start_with_underscore.severity = warning 6 | 7 | dotnet_naming_symbols.private_fields.applicable_kinds = field 8 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 9 | 10 | dotnet_naming_style.starts_with_underscore.capitalization = camel_case 11 | dotnet_naming_style.starts_with_underscore.required_prefix = _ 12 | -------------------------------------------------------------------------------- /src/msc/msc.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | Exe 6 | false 7 | Minsk 8 | Minsk 9 | netcoreapp3.1 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ParameterSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class ParameterSyntax : SyntaxNode 4 | { 5 | internal ParameterSyntax(SyntaxTree syntaxTree, SyntaxToken identifier, TypeClauseSyntax type) 6 | : base(syntaxTree) 7 | { 8 | Identifier = identifier; 9 | Type = type; 10 | } 11 | 12 | public override SyntaxKind Kind => SyntaxKind.Parameter; 13 | public SyntaxToken Identifier { get; } 14 | public TypeClauseSyntax Type { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundLoopStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal abstract class BoundLoopStatement : BoundStatement 6 | { 7 | protected BoundLoopStatement(SyntaxNode syntax, BoundLabel breakLabel, BoundLabel continueLabel) 8 | : base(syntax) 9 | { 10 | BreakLabel = breakLabel; 11 | ContinueLabel = continueLabel; 12 | } 13 | 14 | public BoundLabel BreakLabel { get; } 15 | public BoundLabel ContinueLabel { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Minsk.Tests/CodeAnalysis/Text/SourceTextTests.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Text; 2 | using Xunit; 3 | 4 | namespace Minsk.Tests.CodeAnalysis.Text 5 | { 6 | public class SourceTextTests 7 | { 8 | [Theory] 9 | [InlineData(".", 1)] 10 | [InlineData(".\r\n", 2)] 11 | [InlineData(".\r\n\r\n", 3)] 12 | public void SourceText_IncludesLastLine(string text, int expectedLineCount) 13 | { 14 | var sourceText = SourceText.From(text); 15 | Assert.Equal(expectedLineCount, sourceText.Lines.Length); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/DiagnosticExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | 5 | namespace Minsk.CodeAnalysis 6 | { 7 | public static class DiagnosticExtensions 8 | { 9 | public static bool HasErrors(this ImmutableArray diagnostics) 10 | { 11 | return diagnostics.Any(d => d.IsError); 12 | } 13 | 14 | public static bool HasErrors(this IEnumerable diagnostics) 15 | { 16 | return diagnostics.Any(d => d.IsError); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/TypeClauseSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class TypeClauseSyntax : SyntaxNode 4 | { 5 | internal TypeClauseSyntax(SyntaxTree syntaxTree, SyntaxToken colonToken, SyntaxToken identifier) 6 | : base(syntaxTree) 7 | { 8 | ColonToken = colonToken; 9 | Identifier = identifier; 10 | } 11 | 12 | public override SyntaxKind Kind => SyntaxKind.TypeClause; 13 | public SyntaxToken ColonToken { get; } 14 | public SyntaxToken Identifier { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundBlockStatement.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal sealed class BoundBlockStatement : BoundStatement 7 | { 8 | public BoundBlockStatement(SyntaxNode syntax, ImmutableArray statements) 9 | : base(syntax) 10 | { 11 | Statements = statements; 12 | } 13 | 14 | public override BoundNodeKind Kind => BoundNodeKind.BlockStatement; 15 | public ImmutableArray Statements { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ElseClauseSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class ElseClauseSyntax : SyntaxNode 4 | { 5 | internal ElseClauseSyntax(SyntaxTree syntaxTree, SyntaxToken elseKeyword, StatementSyntax elseStatement) 6 | : base(syntaxTree) 7 | { 8 | ElseKeyword = elseKeyword; 9 | ElseStatement = elseStatement; 10 | } 11 | 12 | public override SyntaxKind Kind => SyntaxKind.ElseClause; 13 | public SyntaxToken ElseKeyword { get; } 14 | public StatementSyntax ElseStatement { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Minsk.Tests/Minsk.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | netcoreapp3.1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/UnaryExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class UnaryExpressionSyntax : ExpressionSyntax 4 | { 5 | internal UnaryExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken operatorToken, ExpressionSyntax operand) 6 | : base(syntaxTree) 7 | { 8 | OperatorToken = operatorToken; 9 | Operand = operand; 10 | } 11 | 12 | public override SyntaxKind Kind => SyntaxKind.UnaryExpression; 13 | public SyntaxToken OperatorToken { get; } 14 | public ExpressionSyntax Operand { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ReturnStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class ReturnStatementSyntax : StatementSyntax 4 | { 5 | internal ReturnStatementSyntax(SyntaxTree syntaxTree, SyntaxToken returnKeyword, ExpressionSyntax? expression) 6 | : base(syntaxTree) 7 | { 8 | ReturnKeyword = returnKeyword; 9 | Expression = expression; 10 | } 11 | 12 | public override SyntaxKind Kind => SyntaxKind.ReturnStatement; 13 | public SyntaxToken ReturnKeyword { get; } 14 | public ExpressionSyntax? Expression { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/VariableSymbol.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Binding; 3 | 4 | namespace Minsk.CodeAnalysis.Symbols 5 | { 6 | public abstract class VariableSymbol : Symbol 7 | { 8 | internal VariableSymbol(string name, bool isReadOnly, TypeSymbol type, BoundConstant? constant) 9 | : base(name) 10 | { 11 | IsReadOnly = isReadOnly; 12 | Type = type; 13 | Constant = isReadOnly ? constant : null; 14 | } 15 | 16 | public bool IsReadOnly { get; } 17 | public TypeSymbol Type { get; } 18 | internal BoundConstant? Constant { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/Minsk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | netcoreapp3.1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundErrorExpression.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Symbols; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | // TODO: Should the error expression accept an array of bound nodes so that we don't drop 7 | // parts of the bound tree on the floor? 8 | 9 | internal sealed class BoundErrorExpression : BoundExpression 10 | { 11 | public BoundErrorExpression(SyntaxNode syntax) 12 | : base(syntax) 13 | { 14 | } 15 | 16 | public override BoundNodeKind Kind => BoundNodeKind.ErrorExpression; 17 | public override TypeSymbol Type => TypeSymbol.Error; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundNode.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal abstract class BoundNode 7 | { 8 | protected BoundNode(SyntaxNode syntax) 9 | { 10 | Syntax = syntax; 11 | } 12 | 13 | public abstract BoundNodeKind Kind { get; } 14 | public SyntaxNode Syntax { get; } 15 | 16 | public override string ToString() 17 | { 18 | using (var writer = new StringWriter()) 19 | { 20 | this.WriteTo(writer); 21 | return writer.ToString(); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/CompilationUnitSyntax.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Minsk.CodeAnalysis.Syntax 4 | { 5 | public sealed partial class CompilationUnitSyntax : SyntaxNode 6 | { 7 | internal CompilationUnitSyntax(SyntaxTree syntaxTree, ImmutableArray members, SyntaxToken endOfFileToken) 8 | : base(syntaxTree) 9 | { 10 | Members = members; 11 | EndOfFileToken = endOfFileToken; 12 | } 13 | 14 | public override SyntaxKind Kind => SyntaxKind.CompilationUnit; 15 | public ImmutableArray Members { get; } 16 | public SyntaxToken EndOfFileToken { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundConversionExpression.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Symbols; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal sealed class BoundConversionExpression : BoundExpression 7 | { 8 | public BoundConversionExpression(SyntaxNode syntax, TypeSymbol type, BoundExpression expression) 9 | : base(syntax) 10 | { 11 | Type = type; 12 | Expression = expression; 13 | } 14 | 15 | public override BoundNodeKind Kind => BoundNodeKind.ConversionExpression; 16 | public override TypeSymbol Type { get; } 17 | public BoundExpression Expression { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/SyntaxTrivia.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Text; 2 | 3 | namespace Minsk.CodeAnalysis.Syntax 4 | { 5 | public sealed class SyntaxTrivia 6 | { 7 | internal SyntaxTrivia(SyntaxTree syntaxTree, SyntaxKind kind, int position, string text) 8 | { 9 | SyntaxTree = syntaxTree; 10 | Kind = kind; 11 | Position = position; 12 | Text = text; 13 | } 14 | 15 | public SyntaxTree SyntaxTree { get; } 16 | public SyntaxKind Kind { get; } 17 | public int Position { get; } 18 | public TextSpan Span => new TextSpan(Position, Text?.Length ?? 0); 19 | public string Text { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Text/TextLocation.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Text 2 | { 3 | public struct TextLocation 4 | { 5 | public TextLocation(SourceText text, TextSpan span) 6 | { 7 | Text = text; 8 | Span = span; 9 | } 10 | 11 | public SourceText Text { get; } 12 | public TextSpan Span { get; } 13 | 14 | public string FileName => Text.FileName; 15 | public int StartLine => Text.GetLineIndex(Span.Start); 16 | public int StartCharacter => Span.Start - Text.Lines[StartLine].Start; 17 | public int EndLine => Text.GetLineIndex(Span.End); 18 | public int EndCharacter => Span.End - Text.Lines[EndLine].Start; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundVariableDeclaration.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Symbols; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal sealed class BoundVariableDeclaration : BoundStatement 7 | { 8 | public BoundVariableDeclaration(SyntaxNode syntax, VariableSymbol variable, BoundExpression initializer) 9 | : base(syntax) 10 | { 11 | Variable = variable; 12 | Initializer = initializer; 13 | } 14 | 15 | public override BoundNodeKind Kind => BoundNodeKind.VariableDeclaration; 16 | public VariableSymbol Variable { get; } 17 | public BoundExpression Initializer { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundWhileStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundWhileStatement : BoundLoopStatement 6 | { 7 | public BoundWhileStatement(SyntaxNode syntax, BoundExpression condition, BoundStatement body, BoundLabel breakLabel, BoundLabel continueLabel) 8 | : base(syntax, breakLabel, continueLabel) 9 | { 10 | Condition = condition; 11 | Body = body; 12 | } 13 | 14 | public override BoundNodeKind Kind => BoundNodeKind.WhileStatement; 15 | public BoundExpression Condition { get; } 16 | public BoundStatement Body { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundDoWhileStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundDoWhileStatement : BoundLoopStatement 6 | { 7 | public BoundDoWhileStatement(SyntaxNode syntax, BoundStatement body, BoundExpression condition, BoundLabel breakLabel, BoundLabel continueLabel) 8 | : base(syntax, breakLabel, continueLabel) 9 | { 10 | Body = body; 11 | Condition = condition; 12 | } 13 | 14 | public override BoundNodeKind Kind => BoundNodeKind.DoWhileStatement; 15 | public BoundStatement Body { get; } 16 | public BoundExpression Condition { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/BinaryExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class BinaryExpressionSyntax : ExpressionSyntax 4 | { 5 | internal BinaryExpressionSyntax(SyntaxTree syntaxTree, ExpressionSyntax left, SyntaxToken operatorToken, ExpressionSyntax right) 6 | : base(syntaxTree) 7 | { 8 | Left = left; 9 | OperatorToken = operatorToken; 10 | Right = right; 11 | } 12 | 13 | public override SyntaxKind Kind => SyntaxKind.BinaryExpression; 14 | public ExpressionSyntax Left { get; } 15 | public SyntaxToken OperatorToken { get; } 16 | public ExpressionSyntax Right { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/WhileStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class WhileStatementSyntax : StatementSyntax 4 | { 5 | internal WhileStatementSyntax(SyntaxTree syntaxTree, SyntaxToken whileKeyword, ExpressionSyntax condition, StatementSyntax body) 6 | : base(syntaxTree) 7 | { 8 | WhileKeyword = whileKeyword; 9 | Condition = condition; 10 | Body = body; 11 | } 12 | 13 | public override SyntaxKind Kind => SyntaxKind.WhileStatement; 14 | public SyntaxToken WhileKeyword { get; } 15 | public ExpressionSyntax Condition { get; } 16 | public StatementSyntax Body { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundVariableExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundVariableExpression : BoundExpression 8 | { 9 | public BoundVariableExpression(SyntaxNode syntax, VariableSymbol variable) 10 | : base(syntax) 11 | { 12 | Variable = variable; 13 | } 14 | 15 | public override BoundNodeKind Kind => BoundNodeKind.VariableExpression; 16 | public override TypeSymbol Type => Variable.Type; 17 | public VariableSymbol Variable { get; } 18 | public override BoundConstant? ConstantValue => Variable.Constant; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/Symbol.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Minsk.CodeAnalysis.Symbols 4 | { 5 | public abstract class Symbol 6 | { 7 | private protected Symbol(string name) 8 | { 9 | Name = name; 10 | } 11 | 12 | public abstract SymbolKind Kind { get; } 13 | public string Name { get; } 14 | 15 | public void WriteTo(TextWriter writer) 16 | { 17 | SymbolPrinter.WriteTo(this, writer); 18 | } 19 | 20 | public override string ToString() 21 | { 22 | using (var writer = new StringWriter()) 23 | { 24 | WriteTo(writer); 25 | return writer.ToString(); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/TypeSymbol.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Symbols 2 | { 3 | public sealed class TypeSymbol : Symbol 4 | { 5 | public static readonly TypeSymbol Error = new TypeSymbol("?"); 6 | public static readonly TypeSymbol Any = new TypeSymbol("any"); 7 | public static readonly TypeSymbol Bool = new TypeSymbol("bool"); 8 | public static readonly TypeSymbol Int = new TypeSymbol("int"); 9 | public static readonly TypeSymbol String = new TypeSymbol("string"); 10 | public static readonly TypeSymbol Void = new TypeSymbol("void"); 11 | 12 | private TypeSymbol(string name) 13 | : base(name) 14 | { 15 | } 16 | 17 | public override SymbolKind Kind => SymbolKind.Type; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundConditionalGotoStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundConditionalGotoStatement : BoundStatement 6 | { 7 | public BoundConditionalGotoStatement(SyntaxNode syntax, BoundLabel label, BoundExpression condition, bool jumpIfTrue = true) 8 | : base(syntax) 9 | { 10 | Label = label; 11 | Condition = condition; 12 | JumpIfTrue = jumpIfTrue; 13 | } 14 | 15 | public override BoundNodeKind Kind => BoundNodeKind.ConditionalGotoStatement; 16 | public BoundLabel Label { get; } 17 | public BoundExpression Condition { get; } 18 | public bool JumpIfTrue { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundIfStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Syntax; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class BoundIfStatement : BoundStatement 6 | { 7 | public BoundIfStatement(SyntaxNode syntax, BoundExpression condition, BoundStatement thenStatement, BoundStatement? elseStatement) 8 | : base(syntax) 9 | { 10 | Condition = condition; 11 | ThenStatement = thenStatement; 12 | ElseStatement = elseStatement; 13 | } 14 | 15 | public override BoundNodeKind Kind => BoundNodeKind.IfStatement; 16 | public BoundExpression Condition { get; } 17 | public BoundStatement ThenStatement { get; } 18 | public BoundStatement? ElseStatement { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundAssignmentExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundAssignmentExpression : BoundExpression 8 | { 9 | public BoundAssignmentExpression(SyntaxNode syntax, VariableSymbol variable, BoundExpression expression) 10 | : base(syntax) 11 | { 12 | Variable = variable; 13 | Expression = expression; 14 | } 15 | 16 | public override BoundNodeKind Kind => BoundNodeKind.AssignmentExpression; 17 | public override TypeSymbol Type => Expression.Type; 18 | public VariableSymbol Variable { get; } 19 | public BoundExpression Expression { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/AssignmentExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class AssignmentExpressionSyntax : ExpressionSyntax 4 | { 5 | public AssignmentExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken identifierToken, SyntaxToken assignmentToken, ExpressionSyntax expression) 6 | : base(syntaxTree) 7 | { 8 | IdentifierToken = identifierToken; 9 | AssignmentToken = assignmentToken; 10 | Expression = expression; 11 | } 12 | 13 | public override SyntaxKind Kind => SyntaxKind.AssignmentExpression; 14 | public SyntaxToken IdentifierToken { get; } 15 | public SyntaxToken AssignmentToken { get; } 16 | public ExpressionSyntax Expression { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/LiteralExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class LiteralExpressionSyntax : ExpressionSyntax 4 | { 5 | internal LiteralExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken literalToken) 6 | : this(syntaxTree, literalToken, literalToken.Value!) 7 | { 8 | } 9 | 10 | internal LiteralExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken literalToken, object value) 11 | : base(syntaxTree) 12 | { 13 | LiteralToken = literalToken; 14 | Value = value; 15 | } 16 | 17 | public override SyntaxKind Kind => SyntaxKind.LiteralExpression; 18 | public SyntaxToken LiteralToken { get; } 19 | public object Value { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/FunctionSymbol.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Symbols 5 | { 6 | public sealed class FunctionSymbol : Symbol 7 | { 8 | internal FunctionSymbol(string name, ImmutableArray parameters, TypeSymbol type, FunctionDeclarationSyntax? declaration = null) 9 | : base(name) 10 | { 11 | Parameters = parameters; 12 | Type = type; 13 | Declaration = declaration; 14 | } 15 | 16 | public override SymbolKind Kind => SymbolKind.Function; 17 | public FunctionDeclarationSyntax? Declaration { get; } 18 | public ImmutableArray Parameters { get; } 19 | public TypeSymbol Type { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundCallExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundCallExpression : BoundExpression 8 | { 9 | public BoundCallExpression(SyntaxNode syntax, FunctionSymbol function, ImmutableArray arguments) 10 | : base(syntax) 11 | { 12 | Function = function; 13 | Arguments = arguments; 14 | } 15 | 16 | public override BoundNodeKind Kind => BoundNodeKind.CallExpression; 17 | public override TypeSymbol Type => Function.Type; 18 | public FunctionSymbol Function { get; } 19 | public ImmutableArray Arguments { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundNodeKind.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Binding 2 | { 3 | internal enum BoundNodeKind 4 | { 5 | // Statements 6 | BlockStatement, 7 | NopStatement, 8 | VariableDeclaration, 9 | IfStatement, 10 | WhileStatement, 11 | DoWhileStatement, 12 | ForStatement, 13 | LabelStatement, 14 | GotoStatement, 15 | ConditionalGotoStatement, 16 | ReturnStatement, 17 | ExpressionStatement, 18 | 19 | // Expressions 20 | ErrorExpression, 21 | LiteralExpression, 22 | VariableExpression, 23 | AssignmentExpression, 24 | CompoundAssignmentExpression, 25 | UnaryExpression, 26 | BinaryExpression, 27 | CallExpression, 28 | ConversionExpression, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/DoWhileStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class DoWhileStatementSyntax : StatementSyntax 4 | { 5 | internal DoWhileStatementSyntax(SyntaxTree syntaxTree, SyntaxToken doKeyword, StatementSyntax body, SyntaxToken whileKeyword, ExpressionSyntax condition) 6 | : base(syntaxTree) 7 | { 8 | DoKeyword = doKeyword; 9 | Body = body; 10 | WhileKeyword = whileKeyword; 11 | Condition = condition; 12 | } 13 | 14 | public override SyntaxKind Kind => SyntaxKind.DoWhileStatement; 15 | public SyntaxToken DoKeyword { get; } 16 | public StatementSyntax Body { get; } 17 | public SyntaxToken WhileKeyword { get; } 18 | public ExpressionSyntax Condition { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/BlockStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Minsk.CodeAnalysis.Syntax 4 | { 5 | public sealed partial class BlockStatementSyntax : StatementSyntax 6 | { 7 | internal BlockStatementSyntax(SyntaxTree syntaxTree, SyntaxToken openBraceToken, ImmutableArray statements, SyntaxToken closeBraceToken) 8 | : base(syntaxTree) 9 | { 10 | OpenBraceToken = openBraceToken; 11 | Statements = statements; 12 | CloseBraceToken = closeBraceToken; 13 | } 14 | 15 | public override SyntaxKind Kind => SyntaxKind.BlockStatement; 16 | public SyntaxToken OpenBraceToken { get; } 17 | public ImmutableArray Statements { get; } 18 | public SyntaxToken CloseBraceToken { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Text/TextSpan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minsk.CodeAnalysis.Text 4 | { 5 | public struct TextSpan 6 | { 7 | public TextSpan(int start, int length) 8 | { 9 | Start = start; 10 | Length = length; 11 | } 12 | 13 | public int Start { get; } 14 | public int Length { get; } 15 | public int End => Start + Length; 16 | 17 | public static TextSpan FromBounds(int start, int end) 18 | { 19 | var length = end - start; 20 | return new TextSpan(start, length); 21 | } 22 | 23 | public bool OverlapsWith(TextSpan span) 24 | { 25 | return Start < span.End && 26 | End > span.Start; 27 | } 28 | 29 | public override string ToString() => $"{Start}..{End}"; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ParenthesizedExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class ParenthesizedExpressionSyntax : ExpressionSyntax 4 | { 5 | internal ParenthesizedExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken openParenthesisToken, ExpressionSyntax expression, SyntaxToken closeParenthesisToken) 6 | : base(syntaxTree) 7 | { 8 | OpenParenthesisToken = openParenthesisToken; 9 | Expression = expression; 10 | CloseParenthesisToken = closeParenthesisToken; 11 | } 12 | 13 | public override SyntaxKind Kind => SyntaxKind.ParenthesizedExpression; 14 | public SyntaxToken OpenParenthesisToken { get; } 15 | public ExpressionSyntax Expression { get; } 16 | public SyntaxToken CloseParenthesisToken { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/IfStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class IfStatementSyntax : StatementSyntax 4 | { 5 | internal IfStatementSyntax(SyntaxTree syntaxTree, SyntaxToken ifKeyword, ExpressionSyntax condition, StatementSyntax thenStatement, ElseClauseSyntax? elseClause) 6 | : base(syntaxTree) 7 | { 8 | IfKeyword = ifKeyword; 9 | Condition = condition; 10 | ThenStatement = thenStatement; 11 | ElseClause = elseClause; 12 | } 13 | 14 | public override SyntaxKind Kind => SyntaxKind.IfStatement; 15 | public SyntaxToken IfKeyword { get; } 16 | public ExpressionSyntax Condition { get; } 17 | public StatementSyntax ThenStatement { get; } 18 | public ElseClauseSyntax? ElseClause { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Text/TextLine.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Text 2 | { 3 | public sealed class TextLine 4 | { 5 | public TextLine(SourceText text, int start, int length, int lengthIncludingLineBreak) 6 | { 7 | Text = text; 8 | Start = start; 9 | Length = length; 10 | LengthIncludingLineBreak = lengthIncludingLineBreak; 11 | } 12 | 13 | public SourceText Text { get; } 14 | public int Start { get; } 15 | public int Length { get; } 16 | public int End => Start + Length; 17 | public int LengthIncludingLineBreak { get; } 18 | public TextSpan Span => new TextSpan(Start, Length); 19 | public TextSpan SpanIncludingLineBreak => new TextSpan(Start, LengthIncludingLineBreak); 20 | public override string ToString() => Text.ToString(Span); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundUnaryExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundUnaryExpression : BoundExpression 8 | { 9 | public BoundUnaryExpression(SyntaxNode syntax, BoundUnaryOperator op, BoundExpression operand) 10 | : base(syntax) 11 | { 12 | Op = op; 13 | Operand = operand; 14 | ConstantValue = ConstantFolding.Fold(op, operand); 15 | } 16 | 17 | public override BoundNodeKind Kind => BoundNodeKind.UnaryExpression; 18 | public override TypeSymbol Type => Op.Type; 19 | public BoundUnaryOperator Op { get; } 20 | public BoundExpression Operand { get; } 21 | public override BoundConstant? ConstantValue { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundCompoundAssignmentExpression.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Symbols; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal sealed class BoundCompoundAssignmentExpression : BoundExpression 7 | { 8 | public BoundCompoundAssignmentExpression(SyntaxNode syntax, VariableSymbol variable, BoundBinaryOperator op, BoundExpression expression) 9 | : base(syntax) 10 | { 11 | Variable = variable; 12 | Op = op; 13 | Expression = expression; 14 | } 15 | 16 | public override BoundNodeKind Kind => BoundNodeKind.CompoundAssignmentExpression; 17 | public override TypeSymbol Type => Expression.Type; 18 | public VariableSymbol Variable { get; } 19 | public BoundBinaryOperator Op {get; } 20 | public BoundExpression Expression { get; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/CallExpressionSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class CallExpressionSyntax : ExpressionSyntax 4 | { 5 | internal CallExpressionSyntax(SyntaxTree syntaxTree, SyntaxToken identifier, SyntaxToken openParenthesisToken, SeparatedSyntaxList arguments, SyntaxToken closeParenthesisToken) 6 | : base(syntaxTree) 7 | { 8 | Identifier = identifier; 9 | OpenParenthesisToken = openParenthesisToken; 10 | Arguments = arguments; 11 | CloseParenthesisToken = closeParenthesisToken; 12 | } 13 | 14 | public override SyntaxKind Kind => SyntaxKind.CallExpression; 15 | public SyntaxToken Identifier { get; } 16 | public SyntaxToken OpenParenthesisToken { get; } 17 | public SeparatedSyntaxList Arguments { get; } 18 | public SyntaxToken CloseParenthesisToken { get; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundBinaryExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundBinaryExpression : BoundExpression 8 | { 9 | public BoundBinaryExpression(SyntaxNode syntax, BoundExpression left, BoundBinaryOperator op, BoundExpression right) 10 | : base(syntax) 11 | { 12 | Left = left; 13 | Op = op; 14 | Right = right; 15 | ConstantValue = ConstantFolding.Fold(left, op, right); 16 | } 17 | 18 | public override BoundNodeKind Kind => BoundNodeKind.BinaryExpression; 19 | public override TypeSymbol Type => Op.Type; 20 | public BoundExpression Left { get; } 21 | public BoundBinaryOperator Op { get; } 22 | public BoundExpression Right { get; } 23 | public override BoundConstant? ConstantValue { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundForStatement.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Symbols; 2 | using Minsk.CodeAnalysis.Syntax; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal sealed class BoundForStatement : BoundLoopStatement 7 | { 8 | public BoundForStatement(SyntaxNode syntax, VariableSymbol variable, BoundExpression lowerBound, BoundExpression upperBound, BoundStatement body, BoundLabel breakLabel, BoundLabel continueLabel) 9 | : base(syntax, breakLabel, continueLabel) 10 | { 11 | Variable = variable; 12 | LowerBound = lowerBound; 13 | UpperBound = upperBound; 14 | Body = body; 15 | } 16 | 17 | public override BoundNodeKind Kind => BoundNodeKind.ForStatement; 18 | public VariableSymbol Variable { get; } 19 | public BoundExpression LowerBound { get; } 20 | public BoundExpression UpperBound { get; } 21 | public BoundStatement Body { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/VariableDeclarationSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class VariableDeclarationSyntax : StatementSyntax 4 | { 5 | internal VariableDeclarationSyntax(SyntaxTree syntaxTree, SyntaxToken keyword, SyntaxToken identifier, TypeClauseSyntax? typeClause, SyntaxToken equalsToken, ExpressionSyntax initializer) 6 | : base(syntaxTree) 7 | { 8 | Keyword = keyword; 9 | Identifier = identifier; 10 | TypeClause = typeClause; 11 | EqualsToken = equalsToken; 12 | Initializer = initializer; 13 | } 14 | 15 | public override SyntaxKind Kind => SyntaxKind.VariableDeclaration; 16 | public SyntaxToken Keyword { get; } 17 | public SyntaxToken Identifier { get; } 18 | public TypeClauseSyntax? TypeClause { get; } 19 | public SyntaxToken EqualsToken { get; } 20 | public ExpressionSyntax Initializer { get; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Diagnostic.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Text; 2 | 3 | namespace Minsk.CodeAnalysis 4 | { 5 | public sealed class Diagnostic 6 | { 7 | private Diagnostic(bool isError, TextLocation location, string message) 8 | { 9 | IsError = isError; 10 | Location = location; 11 | Message = message; 12 | IsWarning = !IsError; 13 | } 14 | 15 | public bool IsError { get; } 16 | public TextLocation Location { get; } 17 | public string Message { get; } 18 | public bool IsWarning { get; } 19 | 20 | public override string ToString() => Message; 21 | 22 | public static Diagnostic Error(TextLocation location, string message) 23 | { 24 | return new Diagnostic(isError: true, location, message); 25 | } 26 | 27 | public static Diagnostic Warning(TextLocation location, string message) 28 | { 29 | return new Diagnostic(isError: false, location, message); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Minsk.Tests/CodeAnalysis/Syntax/SyntaxFactTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Minsk.CodeAnalysis.Syntax; 4 | using Xunit; 5 | 6 | namespace Minsk.Tests.CodeAnalysis.Syntax 7 | { 8 | public class SyntaxFactTests 9 | { 10 | [Theory] 11 | [MemberData(nameof(GetSyntaxKindData))] 12 | public void SyntaxFact_GetText_RoundTrips(SyntaxKind kind) 13 | { 14 | var text = SyntaxFacts.GetText(kind); 15 | if (text == null) 16 | return; 17 | 18 | var tokens = SyntaxTree.ParseTokens(text); 19 | var token = Assert.Single(tokens); 20 | Assert.Equal(kind, token.Kind); 21 | Assert.Equal(text, token.Text); 22 | } 23 | 24 | public static IEnumerable GetSyntaxKindData() 25 | { 26 | var kinds = (SyntaxKind[]) Enum.GetValues(typeof(SyntaxKind)); 27 | foreach (var kind in kinds) 28 | yield return new object[]{ kind }; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | root = true 3 | 4 | # All files 5 | [*] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Encoding 10 | charset = utf-8 11 | 12 | # Indentation and spacing 13 | indent_size = 4 14 | indent_style = space 15 | 16 | # New line preferences 17 | end_of_line = crlf 18 | insert_final_newline = false 19 | 20 | # Formatting 21 | trim_final_newline = true 22 | trim_trailing_whitespace = true 23 | 24 | #### FileType EditorConfig Overrides #### 25 | 26 | # Bash files 27 | [*.sh] 28 | end_of_line = lf 29 | insert_final_newline = true 30 | 31 | # Batch files 32 | [*.{bat,cmd}] 33 | end_of_line = crlf 34 | insert_final_newline = true 35 | 36 | # Build files 37 | [*.{*proj,props,targets,items,tasks}] 38 | indent_size = 2 39 | 40 | # Solution files 41 | [*.sln] 42 | indent_style = tab 43 | 44 | # Script files 45 | [*.{ps1,psm1,psd1}] 46 | insert_final_newline = true 47 | 48 | # Git Files 49 | [.git*] 50 | insert_final_newline = true 51 | 52 | # YAML files 53 | [*.{yml,yaml}] 54 | indent_size = 2 55 | insert_final_newline = true 56 | 57 | # END -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundLiteralExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundLiteralExpression : BoundExpression 8 | { 9 | public BoundLiteralExpression(SyntaxNode syntax, object value) 10 | : base(syntax) 11 | { 12 | if (value is bool) 13 | Type = TypeSymbol.Bool; 14 | else if (value is int) 15 | Type = TypeSymbol.Int; 16 | else if (value is string) 17 | Type = TypeSymbol.String; 18 | else 19 | throw new Exception($"Unexpected literal '{value}' of type {value.GetType()}"); 20 | 21 | ConstantValue = new BoundConstant(value); 22 | } 23 | 24 | public override BoundNodeKind Kind => BoundNodeKind.LiteralExpression; 25 | public override TypeSymbol Type { get; } 26 | public object Value => ConstantValue.Value; 27 | public override BoundConstant ConstantValue { get; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Immo Landwerth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/BuiltinFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace Minsk.CodeAnalysis.Symbols 8 | { 9 | internal static class BuiltinFunctions 10 | { 11 | public static readonly FunctionSymbol Print = new FunctionSymbol("print", ImmutableArray.Create(new ParameterSymbol("text", TypeSymbol.Any, 0)), TypeSymbol.Void); 12 | public static readonly FunctionSymbol Input = new FunctionSymbol("input", ImmutableArray.Empty, TypeSymbol.String); 13 | public static readonly FunctionSymbol Rnd = new FunctionSymbol("rnd", ImmutableArray.Create(new ParameterSymbol("max", TypeSymbol.Int, 0)), TypeSymbol.Int); 14 | 15 | internal static IEnumerable GetAll() 16 | => typeof(BuiltinFunctions).GetFields(BindingFlags.Public | BindingFlags.Static) 17 | .Where(f => f.FieldType == typeof(FunctionSymbol)) 18 | .Select(f => (FunctionSymbol)f.GetValue(null)!); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundProgram.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using Minsk.CodeAnalysis.Symbols; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundProgram 8 | { 9 | public BoundProgram(BoundProgram? previous, 10 | ImmutableArray diagnostics, 11 | FunctionSymbol? mainFunction, 12 | FunctionSymbol? scriptFunction, 13 | ImmutableDictionary functions) 14 | { 15 | Previous = previous; 16 | Diagnostics = diagnostics; 17 | MainFunction = mainFunction; 18 | ScriptFunction = scriptFunction; 19 | Functions = functions; 20 | } 21 | 22 | public BoundProgram? Previous { get; } 23 | public ImmutableArray Diagnostics { get; } 24 | public FunctionSymbol? MainFunction { get; } 25 | public FunctionSymbol? ScriptFunction { get; } 26 | public ImmutableDictionary Functions { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/ForStatementSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class ForStatementSyntax : StatementSyntax 4 | { 5 | internal ForStatementSyntax(SyntaxTree syntaxTree, SyntaxToken keyword, SyntaxToken identifier, SyntaxToken equalsToken, ExpressionSyntax lowerBound, SyntaxToken toKeyword, ExpressionSyntax upperBound, StatementSyntax body) 6 | : base(syntaxTree) 7 | { 8 | Keyword = keyword; 9 | Identifier = identifier; 10 | EqualsToken = equalsToken; 11 | LowerBound = lowerBound; 12 | ToKeyword = toKeyword; 13 | UpperBound = upperBound; 14 | Body = body; 15 | } 16 | 17 | public override SyntaxKind Kind => SyntaxKind.ForStatement; 18 | public SyntaxToken Keyword { get; } 19 | public SyntaxToken Identifier { get; } 20 | public SyntaxToken EqualsToken { get; } 21 | public ExpressionSyntax LowerBound { get; } 22 | public SyntaxToken ToKeyword { get; } 23 | public ExpressionSyntax UpperBound { get; } 24 | public StatementSyntax Body { get; } 25 | } 26 | } -------------------------------------------------------------------------------- /samples/samples.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.30011.22 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "hello", "hello\hello.msproj", "{9DB9B226-6BB1-4EA3-ABFF-9F88E50333A6}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {9DB9B226-6BB1-4EA3-ABFF-9F88E50333A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {9DB9B226-6BB1-4EA3-ABFF-9F88E50333A6}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {9DB9B226-6BB1-4EA3-ABFF-9F88E50333A6}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {9DB9B226-6BB1-4EA3-ABFF-9F88E50333A6}.Release|Any CPU.Build.0 = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(SolutionProperties) = preSolution 19 | HideSolutionNode = FALSE 20 | EndGlobalSection 21 | GlobalSection(ExtensibilityGlobals) = postSolution 22 | SolutionGuid = {2D82F985-1252-43DB-BA06-23039810FBE3} 23 | EndGlobalSection 24 | EndGlobal 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/msi/bin/Debug/msi.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/msi", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "externalTerminal", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/FunctionDeclarationSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public sealed partial class FunctionDeclarationSyntax : MemberSyntax 4 | { 5 | internal FunctionDeclarationSyntax(SyntaxTree syntaxTree, SyntaxToken functionKeyword, SyntaxToken identifier, SyntaxToken openParenthesisToken, SeparatedSyntaxList parameters, SyntaxToken closeParenthesisToken, TypeClauseSyntax? type, BlockStatementSyntax body) 6 | : base(syntaxTree) 7 | { 8 | FunctionKeyword = functionKeyword; 9 | Identifier = identifier; 10 | OpenParenthesisToken = openParenthesisToken; 11 | Parameters = parameters; 12 | CloseParenthesisToken = closeParenthesisToken; 13 | Type = type; 14 | Body = body; 15 | } 16 | 17 | public override SyntaxKind Kind => SyntaxKind.FunctionDeclaration; 18 | 19 | public SyntaxToken FunctionKeyword { get; } 20 | public SyntaxToken Identifier { get; } 21 | public SyntaxToken OpenParenthesisToken { get; } 22 | public SeparatedSyntaxList Parameters { get; } 23 | public SyntaxToken CloseParenthesisToken { get; } 24 | public TypeClauseSyntax? Type { get; } 25 | public BlockStatementSyntax Body { get; } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | @(Compile->'"%(Identity)"', ' ') 15 | $(MinskCompilerArgs) /o "@(IntermediateAssembly)" 16 | $(MinskCompilerArgs) @(ReferencePath->'/r "%(Identity)"', ' ') 17 | 18 | .sh 19 | .cmd 20 | msc$(MinskScriptExt) 21 | "$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)..\', '$(MinskCompilerScript)'))" $(MinskCompilerArgs) 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Minsk.Generators/CurlyIndenter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CodeDom.Compiler; 3 | 4 | namespace Minsk.Generators 5 | { 6 | /// 7 | /// Takes care of opening and closing curly braces for code generation 8 | /// 9 | internal class CurlyIndenter : IDisposable 10 | { 11 | private IndentedTextWriter _indentedTextWriter; 12 | 13 | /// 14 | /// Default constructor that maked a tidies creation of the line before the opening curly 15 | /// 16 | /// The writer to use 17 | /// any line to write before the curly 18 | public CurlyIndenter(IndentedTextWriter indentedTextWriter, string openingLine = "") 19 | { 20 | _indentedTextWriter = indentedTextWriter; 21 | if (!string.IsNullOrWhiteSpace(openingLine)) 22 | indentedTextWriter.WriteLine(openingLine); 23 | indentedTextWriter.WriteLine("{"); 24 | indentedTextWriter.Indent++; 25 | } 26 | 27 | /// 28 | /// When the variable goes out of scope the closing brace is injected and indentation reduced. 29 | /// 30 | public void Dispose() 31 | { 32 | _indentedTextWriter.Indent--; 33 | _indentedTextWriter.WriteLine("}"); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundGlobalScope.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Minsk.CodeAnalysis.Symbols; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal sealed class BoundGlobalScope 7 | { 8 | public BoundGlobalScope(BoundGlobalScope? previous, 9 | ImmutableArray diagnostics, 10 | FunctionSymbol? mainFunction, 11 | FunctionSymbol? scriptFunction, 12 | ImmutableArray functions, 13 | ImmutableArray variables, 14 | ImmutableArray statements) 15 | { 16 | Previous = previous; 17 | Diagnostics = diagnostics; 18 | MainFunction = mainFunction; 19 | ScriptFunction = scriptFunction; 20 | Functions = functions; 21 | Variables = variables; 22 | Statements = statements; 23 | } 24 | 25 | public BoundGlobalScope? Previous { get; } 26 | public ImmutableArray Diagnostics { get; } 27 | public FunctionSymbol? MainFunction { get; } 28 | public FunctionSymbol? ScriptFunction { get; } 29 | public ImmutableArray Functions { get; } 30 | public ImmutableArray Variables { get; } 31 | public ImmutableArray Statements { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/episode-09.md: -------------------------------------------------------------------------------- 1 | # Episode 9 2 | 3 | [Video](https://www.youtube.com/watch?v=QwZuY1dExAc&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=9) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/40) | 5 | [Previous](episode-08.md) | 6 | [Next](episode-10.md) 7 | 8 | ## Completed items 9 | 10 | This episode doesn't have much to do with compiler building. We just made the 11 | REPL a bit easier to use. This includes the ability to edit multiple lines, have 12 | history, and syntax highlighting. 13 | 14 | ## Interesting aspects 15 | 16 | ### Two classes 17 | 18 | The REPL is split into two classes: 19 | 20 | * [Repl] is a generic REPL editor and deals with the interception of keys and 21 | rendering. 22 | * [MinskRepl] contains the Minsk specific portion, specifically evaluating the 23 | expressions, keeping track of previous compilations, and using the parser to 24 | decide whether a submission is complete. 25 | 26 | I haven't done this to reuse the REPL, but to make it easier to maintain. It's 27 | not great if the language specific aspects of the REPL are mixed with the 28 | tedious components of key processing and output rendering. 29 | 30 | ## Document/View 31 | 32 | The REPL uses a simple document/view architecture to update the output of the 33 | screen whenever the document changes. 34 | 35 | [Repl]: https://github.com/terrajobst/minsk/blob/69123841304be0b9be0c5dc451c20fa07742f567/src/mc/Repl.cs 36 | [MinskRepl]: https://github.com/terrajobst/minsk/blob/69123841304be0b9be0c5dc451c20fa07742f567/src/mc/MinskRepl.cs -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Strict 5 | Enable 6 | Preview 7 | 8 | 9 | 13 | 14 | false 15 | false 16 | 17 | 18 | 35 | 36 | **/*.g.cs 37 | $(DefaultItemExcludes);$(GeneratedSources) 38 | 39 | 40 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core 2 | 3 | variables: 4 | sln: './src/minsk.sln' 5 | tests: './src/Minsk.Tests/Minsk.Tests.csproj' 6 | samples: './samples/samples.sln' 7 | 8 | parameters: 9 | - name: operatingSystems 10 | type: object 11 | default: 12 | - name: Ubuntu 13 | vmImage: ubuntu-latest 14 | - name: Windows 15 | vmImage: windows-latest 16 | - name: macOS 17 | vmImage: macOS-latest 18 | - name: configurations 19 | type: object 20 | default: 21 | - Debug 22 | - Release 23 | 24 | jobs: 25 | - ${{ each c in parameters.configurations }}: 26 | - ${{ each os in parameters.operatingSystems }}: 27 | - job: job${{ os.name }}${{ c }} 28 | displayName: ${{ c }} (${{ os.name }}) 29 | pool: 30 | vmImage: ${{ os.vmImage }} 31 | steps: 32 | - task: DotNetCoreCLI@2 33 | displayName: Build minsk (${{ os.name }} ${{ c }}) 34 | inputs: 35 | command: build 36 | projects: $(sln) 37 | arguments: --configuration ${{ c }} 38 | - task: DotNetCoreCLI@2 39 | displayName: Run tests (${{ os.name }} ${{ c }}) 40 | inputs: 41 | command: test 42 | projects: $(tests) 43 | arguments: --configuration ${{ c }} 44 | publishTestResults: true 45 | - task: DotNetCoreCLI@2 46 | displayName: Build samples (${{ os.name }} ${{ c }}) 47 | inputs: 48 | command: build 49 | projects: $(samples) 50 | arguments: --configuration ${{ c }} 51 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/SeparatedSyntaxList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | 6 | namespace Minsk.CodeAnalysis.Syntax 7 | { 8 | public abstract class SeparatedSyntaxList 9 | { 10 | private protected SeparatedSyntaxList() 11 | { 12 | } 13 | 14 | public abstract ImmutableArray GetWithSeparators(); 15 | } 16 | 17 | public sealed class SeparatedSyntaxList : SeparatedSyntaxList, IEnumerable 18 | where T: SyntaxNode 19 | { 20 | private readonly ImmutableArray _nodesAndSeparators; 21 | 22 | internal SeparatedSyntaxList(ImmutableArray nodesAndSeparators) 23 | { 24 | _nodesAndSeparators = nodesAndSeparators; 25 | } 26 | 27 | public int Count => (_nodesAndSeparators.Length + 1) / 2; 28 | 29 | public T this[int index] => (T) _nodesAndSeparators[index * 2]; 30 | 31 | public SyntaxToken GetSeparator(int index) 32 | { 33 | if (index < 0 || index >= Count - 1) 34 | throw new ArgumentOutOfRangeException(nameof(index)); 35 | 36 | return (SyntaxToken) _nodesAndSeparators[index * 2 + 1]; 37 | } 38 | 39 | public override ImmutableArray GetWithSeparators() => _nodesAndSeparators; 40 | 41 | public IEnumerator GetEnumerator() 42 | { 43 | for (var i = 0; i < Count; i++) 44 | yield return this[i]; 45 | } 46 | 47 | IEnumerator IEnumerable.GetEnumerator() 48 | { 49 | return GetEnumerator(); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /azure-pipelines-index.yml: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core 2 | 3 | pool: 4 | vmImage: 'windows-latest' 5 | 6 | variables: 7 | buildConfiguration: 'Release' 8 | sln: './src/minsk.sln' 9 | 10 | # We only want to kick off indexing runs for Master 11 | trigger: ['master'] 12 | 13 | # We don't want to build PRs 14 | pr: none 15 | 16 | steps: 17 | - task: DotNetCoreCLI@2 18 | displayName: Build minsk ($(buildConfiguration)) 19 | inputs: 20 | command: build 21 | projects: $(sln) 22 | arguments: -c $(buildConfiguration) 23 | - task: PowerShell@2 24 | displayName: Generate SourceBrowser Index 25 | inputs: 26 | filePath: 'genindex.ps1' 27 | - task: FtpUpload@2 28 | displayName: Upload SourceBrowser Index 29 | inputs: 30 | credentialsOption: 'serviceEndpoint' 31 | serverEndpoint: 'FTP Minsk Index' 32 | rootDirectory: 'bin\index\index' 33 | filePatterns: '**' 34 | remoteDirectory: '/site/wwwroot/index' 35 | clean: false 36 | cleanContents: true 37 | preservePaths: true 38 | trustSSL: true 39 | - task: AzureAppServiceManage@0 40 | inputs: 41 | azureSubscription: 'Visual Studio Enterprise(5ea88856-cbf8-44e5-8d86-35a35073a080)' 42 | Action: 'Restart Azure App Service' 43 | WebAppName: 'minsk-source' 44 | SpecifySlotOrASE: true 45 | ResourceGroupName: 'Minsk-SourceBrowser-ResourceGroup' 46 | Slot: 'staging' 47 | - task: AzureAppServiceManage@0 48 | inputs: 49 | azureSubscription: 'Visual Studio Enterprise(5ea88856-cbf8-44e5-8d86-35a35073a080)' 50 | Action: 'Swap Slots' 51 | WebAppName: 'minsk-source' 52 | ResourceGroupName: 'Minsk-SourceBrowser-ResourceGroup' 53 | SourceSlot: 'staging' 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minsk 2 | 3 | [![Build Status](https://terrajobst.visualstudio.com/Minsk/_apis/build/status/terrajobst.minsk?branchName=master)](https://terrajobst.visualstudio.com/Minsk/_build/latest?definitionId=13) 4 | 5 | > Have you considered Minsk? -- Worf, [naming things][ds9-minsk]. 6 | 7 | This repo contains **Minsk**, a handwritten compiler in C#. It illustrates basic 8 | concepts of compiler construction and how one can tool the language inside of an 9 | IDE by exposing APIs for parsing and type checking. 10 | 11 | This compiler uses many of the concepts that you can find in the Microsoft 12 | C# and Visual Basic compilers, code named [Roslyn]. 13 | 14 | [ds9-minsk]: https://www.youtube.com/watch?v=138gX3wolOo 15 | [Roslyn]: https://github.com/dotnet/roslyn 16 | 17 | ## Live coding 18 | 19 | This code base was written live during streaming. You can watch the recordings 20 | on [YouTube] or browse the [episode PRs][episodes]. 21 | 22 | [YouTube]: https://www.youtube.com/playlist?list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y 23 | [episodes]: https://github.com/terrajobst/minsk/pulls?q=is%3Apr+is%3Aclosed+label%3Aepisode+sort%3Acreated-asc 24 | 25 | ## Browsing the code 26 | 27 | If you want to browse the code, check out the symbolic [source browser]. 28 | 29 | [source browser]: http://source.minsk-compiler.net 30 | 31 | ## Donations 32 | 33 | Some people kindly asked me whether I accept donations. I have the luxury of 34 | working for a great employer and I make a good salary. That means I have got the 35 | time and means to produce these videos and share my passion for open source and 36 | .NET. 37 | 38 | But not everyone has that luxury. If you find these videos helpful and you want 39 | to give back, consider donating to organizations that help the less fortunate to 40 | get into the tech industry, such as [Black Girls Code]. 41 | 42 | Thank you ❤ 43 | 44 | [Black Girls Code]: http://www.blackgirlscode.com/donations.html 45 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundUnaryOperator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundUnaryOperator 8 | { 9 | private BoundUnaryOperator(SyntaxKind syntaxKind, BoundUnaryOperatorKind kind, TypeSymbol operandType) 10 | : this(syntaxKind, kind, operandType, operandType) 11 | { 12 | } 13 | 14 | private BoundUnaryOperator(SyntaxKind syntaxKind, BoundUnaryOperatorKind kind, TypeSymbol operandType, TypeSymbol resultType) 15 | { 16 | SyntaxKind = syntaxKind; 17 | Kind = kind; 18 | OperandType = operandType; 19 | Type = resultType; 20 | } 21 | 22 | public SyntaxKind SyntaxKind { get; } 23 | public BoundUnaryOperatorKind Kind { get; } 24 | public TypeSymbol OperandType { get; } 25 | public TypeSymbol Type { get; } 26 | 27 | private static BoundUnaryOperator[] _operators = 28 | { 29 | new BoundUnaryOperator(SyntaxKind.BangToken, BoundUnaryOperatorKind.LogicalNegation, TypeSymbol.Bool), 30 | 31 | new BoundUnaryOperator(SyntaxKind.PlusToken, BoundUnaryOperatorKind.Identity, TypeSymbol.Int), 32 | new BoundUnaryOperator(SyntaxKind.MinusToken, BoundUnaryOperatorKind.Negation, TypeSymbol.Int), 33 | new BoundUnaryOperator(SyntaxKind.TildeToken, BoundUnaryOperatorKind.OnesComplement, TypeSymbol.Int), 34 | }; 35 | 36 | public static BoundUnaryOperator? Bind(SyntaxKind syntaxKind, TypeSymbol operandType) 37 | { 38 | foreach (var op in _operators) 39 | { 40 | if (op.SyntaxKind == syntaxKind && op.OperandType == operandType) 41 | return op; 42 | } 43 | 44 | return null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/episode-26.md: -------------------------------------------------------------------------------- 1 | # Episode 26 2 | 3 | [Video](https://www.youtube.com/watch?v=Y2Gn6qr_twA&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=26) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/148) | 5 | [Previous](episode-26.md) | 6 | [Next](episode-27.md) 7 | 8 | ## Completed items 9 | 10 | * Enabled nullable in Minsk.Tests & Minsk.Generators 11 | * Honored nullable annotations in source generator 12 | * Filed and fixed various TODOs 13 | 14 | ## Interesting aspects 15 | 16 | ### Leveraging nullable annotations inside the source generator 17 | 18 | We're now honoring nullable annotations when generating the 19 | `SyntaxNode.GetChildren()` methods. 20 | 21 | For example, consider `ReturnStatementSyntax`. The `Expression` property is 22 | marked as being nullable (because `return` can be used without an expression). 23 | 24 | ```C# 25 | partial class ReturnStatementSyntax : StatementSyntax 26 | { 27 | public SyntaxToken ReturnKeyword { get; } 28 | public ExpressionSyntax? Expression { get; } 29 | } 30 | ``` 31 | 32 | Our generator now uses the null annotation for the `Expression` property and 33 | emits a `null` check: 34 | 35 | ```C# 36 | partial class ReturnStatementSyntax 37 | { 38 | public override IEnumerable GetChildren() 39 | { 40 | yield return ReturnKeyword; 41 | if (Expression != null) 42 | yield return Expression; 43 | } 44 | } 45 | ``` 46 | 47 | This is simply done by [using Roslyn's `NullableAnnotation` 48 | property][null-annotations]: 49 | 50 | ```C# 51 | var canBeNull = property.NullableAnnotation == NullableAnnotation.Annotated; 52 | if (canBeNull) 53 | { 54 | writer.WriteLine($"if ({property.Name} != null)"); 55 | writer.Indent++; 56 | } 57 | 58 | writer.WriteLine($"yield return {property.Name};"); 59 | 60 | if (canBeNull) 61 | writer.Indent--; 62 | ``` 63 | 64 | [null-annotations]: https://github.com/terrajobst/minsk/blob/877fefa36e184da125fd62942b5797328df79896/src/Minsk.Generators/SyntaxNodeGetChildrenGenerator.cs#L59-L69 65 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundScope.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using Minsk.CodeAnalysis.Symbols; 5 | 6 | namespace Minsk.CodeAnalysis.Binding 7 | { 8 | internal sealed class BoundScope 9 | { 10 | private Dictionary? _symbols; 11 | 12 | public BoundScope(BoundScope? parent) 13 | { 14 | Parent = parent; 15 | } 16 | 17 | public BoundScope? Parent { get; } 18 | 19 | public bool TryDeclareVariable(VariableSymbol variable) 20 | => TryDeclareSymbol(variable); 21 | 22 | public bool TryDeclareFunction(FunctionSymbol function) 23 | => TryDeclareSymbol(function); 24 | 25 | private bool TryDeclareSymbol(TSymbol symbol) 26 | where TSymbol : Symbol 27 | { 28 | if (_symbols == null) 29 | _symbols = new Dictionary(); 30 | else if (_symbols.ContainsKey(symbol.Name)) 31 | return false; 32 | 33 | _symbols.Add(symbol.Name, symbol); 34 | return true; 35 | } 36 | 37 | public Symbol? TryLookupSymbol(string name) 38 | { 39 | if (_symbols != null && _symbols.TryGetValue(name, out var symbol)) 40 | return symbol; 41 | 42 | return Parent?.TryLookupSymbol(name); 43 | } 44 | 45 | public ImmutableArray GetDeclaredVariables() 46 | => GetDeclaredSymbols(); 47 | 48 | public ImmutableArray GetDeclaredFunctions() 49 | => GetDeclaredSymbols(); 50 | 51 | private ImmutableArray GetDeclaredSymbols() 52 | where TSymbol : Symbol 53 | { 54 | if (_symbols == null) 55 | return ImmutableArray.Empty; 56 | 57 | return _symbols.Values.OfType().ToImmutableArray(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/Conversion.cs: -------------------------------------------------------------------------------- 1 | using Minsk.CodeAnalysis.Symbols; 2 | 3 | namespace Minsk.CodeAnalysis.Binding 4 | { 5 | internal sealed class Conversion 6 | { 7 | public static readonly Conversion None = new Conversion(exists: false, isIdentity: false, isImplicit: false); 8 | public static readonly Conversion Identity = new Conversion(exists: true, isIdentity: true, isImplicit: true); 9 | public static readonly Conversion Implicit = new Conversion(exists: true, isIdentity: false, isImplicit: true); 10 | public static readonly Conversion Explicit = new Conversion(exists: true, isIdentity: false, isImplicit: false); 11 | 12 | private Conversion(bool exists, bool isIdentity, bool isImplicit) 13 | { 14 | Exists = exists; 15 | IsIdentity = isIdentity; 16 | IsImplicit = isImplicit; 17 | } 18 | 19 | public bool Exists { get; } 20 | public bool IsIdentity { get; } 21 | public bool IsImplicit { get; } 22 | public bool IsExplicit => Exists && !IsImplicit; 23 | 24 | public static Conversion Classify(TypeSymbol from, TypeSymbol to) 25 | { 26 | if (from == to) 27 | return Conversion.Identity; 28 | 29 | if (from != TypeSymbol.Void && to == TypeSymbol.Any) 30 | { 31 | return Conversion.Implicit; 32 | } 33 | 34 | if (from == TypeSymbol.Any && to != TypeSymbol.Void) 35 | { 36 | return Conversion.Explicit; 37 | } 38 | 39 | if (from == TypeSymbol.Bool || from == TypeSymbol.Int) 40 | { 41 | if (to == TypeSymbol.String) 42 | return Conversion.Explicit; 43 | } 44 | 45 | if (from == TypeSymbol.String) 46 | { 47 | if (to == TypeSymbol.Bool || to == TypeSymbol.Int) 48 | return Conversion.Explicit; 49 | } 50 | 51 | return Conversion.None; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/SyntaxToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Minsk.CodeAnalysis.Text; 6 | 7 | namespace Minsk.CodeAnalysis.Syntax 8 | { 9 | public sealed class SyntaxToken : SyntaxNode 10 | { 11 | internal SyntaxToken(SyntaxTree syntaxTree, SyntaxKind kind, int position, string? text, object? value, ImmutableArray leadingTrivia, ImmutableArray trailingTrivia) 12 | : base(syntaxTree) 13 | { 14 | Kind = kind; 15 | Position = position; 16 | Text = text ?? string.Empty; 17 | IsMissing = text == null; 18 | Value = value; 19 | LeadingTrivia = leadingTrivia; 20 | TrailingTrivia = trailingTrivia; 21 | } 22 | 23 | public override SyntaxKind Kind { get; } 24 | public int Position { get; } 25 | public string Text { get; } 26 | public object? Value { get; } 27 | public override TextSpan Span => new TextSpan(Position, Text.Length); 28 | public override TextSpan FullSpan 29 | { 30 | get 31 | { 32 | var start = LeadingTrivia.Length == 0 33 | ? Span.Start 34 | : LeadingTrivia.First().Span.Start; 35 | var end = TrailingTrivia.Length == 0 36 | ? Span.End 37 | : TrailingTrivia.Last().Span.End; 38 | return TextSpan.FromBounds(start, end); 39 | } 40 | } 41 | 42 | public ImmutableArray LeadingTrivia { get;} 43 | public ImmutableArray TrailingTrivia { get; } 44 | 45 | public override IEnumerable GetChildren() 46 | { 47 | return Array.Empty(); 48 | } 49 | 50 | /// 51 | /// A token is missing if it was inserted by the parser and doesn't appear in source. 52 | /// 53 | public bool IsMissing { get; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Minsk.Tests/CodeAnalysis/Syntax/AssertingEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Minsk.CodeAnalysis.Syntax; 5 | using Xunit; 6 | 7 | namespace Minsk.Tests.CodeAnalysis.Syntax 8 | { 9 | internal sealed class AssertingEnumerator : IDisposable 10 | { 11 | private readonly IEnumerator _enumerator; 12 | private bool _hasErrors; 13 | 14 | public AssertingEnumerator(SyntaxNode node) 15 | { 16 | _enumerator = Flatten(node).GetEnumerator(); 17 | } 18 | 19 | private bool MarkFailed() 20 | { 21 | _hasErrors = true; 22 | return false; 23 | } 24 | 25 | public void Dispose() 26 | { 27 | if (!_hasErrors) 28 | Assert.False(_enumerator.MoveNext()); 29 | 30 | _enumerator.Dispose(); 31 | } 32 | 33 | private static IEnumerable Flatten(SyntaxNode node) 34 | { 35 | var stack = new Stack(); 36 | stack.Push(node); 37 | 38 | while (stack.Count > 0) 39 | { 40 | var n = stack.Pop(); 41 | yield return n; 42 | 43 | foreach (var child in n.GetChildren().Reverse()) 44 | stack.Push(child); 45 | } 46 | } 47 | 48 | public void AssertNode(SyntaxKind kind) 49 | { 50 | try 51 | { 52 | Assert.True(_enumerator.MoveNext()); 53 | Assert.Equal(kind,_enumerator.Current.Kind); 54 | Assert.IsNotType(_enumerator.Current); 55 | } 56 | catch when (MarkFailed()) 57 | { 58 | throw; 59 | } 60 | } 61 | 62 | public void AssertToken(SyntaxKind kind, string text) 63 | { 64 | try 65 | { 66 | Assert.True(_enumerator.MoveNext()); 67 | Assert.Equal(kind,_enumerator.Current.Kind); 68 | var token = Assert.IsType(_enumerator.Current); 69 | Assert.Equal(text, token.Text); 70 | } 71 | catch when (MarkFailed()) 72 | { 73 | throw; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/SyntaxKind.cs: -------------------------------------------------------------------------------- 1 | namespace Minsk.CodeAnalysis.Syntax 2 | { 3 | public enum SyntaxKind 4 | { 5 | BadToken, 6 | 7 | // Trivia 8 | SkippedTextTrivia, 9 | LineBreakTrivia, 10 | WhitespaceTrivia, 11 | SingleLineCommentTrivia, 12 | MultiLineCommentTrivia, 13 | 14 | // Tokens 15 | EndOfFileToken, 16 | NumberToken, 17 | StringToken, 18 | PlusToken, 19 | PlusEqualsToken, 20 | MinusToken, 21 | MinusEqualsToken, 22 | StarToken, 23 | StarEqualsToken, 24 | SlashToken, 25 | SlashEqualsToken, 26 | BangToken, 27 | EqualsToken, 28 | TildeToken, 29 | HatToken, 30 | HatEqualsToken, 31 | AmpersandToken, 32 | AmpersandAmpersandToken, 33 | AmpersandEqualsToken, 34 | PipeToken, 35 | PipeEqualsToken, 36 | PipePipeToken, 37 | EqualsEqualsToken, 38 | BangEqualsToken, 39 | LessToken, 40 | LessOrEqualsToken, 41 | GreaterToken, 42 | GreaterOrEqualsToken, 43 | OpenParenthesisToken, 44 | CloseParenthesisToken, 45 | OpenBraceToken, 46 | CloseBraceToken, 47 | ColonToken, 48 | CommaToken, 49 | IdentifierToken, 50 | 51 | // Keywords 52 | BreakKeyword, 53 | ContinueKeyword, 54 | ElseKeyword, 55 | FalseKeyword, 56 | ForKeyword, 57 | FunctionKeyword, 58 | IfKeyword, 59 | LetKeyword, 60 | ReturnKeyword, 61 | ToKeyword, 62 | TrueKeyword, 63 | VarKeyword, 64 | WhileKeyword, 65 | DoKeyword, 66 | 67 | // Nodes 68 | CompilationUnit, 69 | FunctionDeclaration, 70 | GlobalStatement, 71 | Parameter, 72 | TypeClause, 73 | ElseClause, 74 | 75 | // Statements 76 | BlockStatement, 77 | VariableDeclaration, 78 | IfStatement, 79 | WhileStatement, 80 | DoWhileStatement, 81 | ForStatement, 82 | BreakStatement, 83 | ContinueStatement, 84 | ReturnStatement, 85 | ExpressionStatement, 86 | 87 | // Expressions 88 | LiteralExpression, 89 | NameExpression, 90 | UnaryExpression, 91 | BinaryExpression, 92 | CompoundAssignmentExpression, 93 | ParenthesizedExpression, 94 | AssignmentExpression, 95 | CallExpression, 96 | } 97 | } -------------------------------------------------------------------------------- /docs/episode-01.md: -------------------------------------------------------------------------------- 1 | # Episode 1 2 | 3 | [Video](https://www.youtube.com/watch?v=wgHIkdUQbp0&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=1&t=19) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/1) | 5 | [Next](episode-02.md) 6 | 7 | ## Completed items 8 | 9 | * Basic REPL (read-eval-print loop) for an expression evaluator 10 | * Added lexer, a parser, and an evaluator 11 | * Handle `+`, `-`, `*`, `/`, and parenthesized expressions 12 | * Print syntax trees 13 | 14 | ## Interesting aspects 15 | 16 | ### Operator precedence 17 | 18 | When parsing the expression `1 + 2 * 3` we need to parse it into a tree 19 | structure that honors priorties, i.e. that `*` binds stronger than `+`: 20 | 21 | ``` 22 | └──BinaryExpression 23 | ├──NumberExpression 24 | │ └──NumberToken 1 25 | ├──PlusToken 26 | └──BinaryExpression 27 | ├──NumberExpression 28 | │ └──NumberToken 2 29 | ├──StarToken 30 | └──NumberExpression 31 | └──NumberToken 3 32 | ``` 33 | 34 | A naive parser might yield something like this: 35 | 36 | ``` 37 | └──BinaryExpression 38 | ├──BinaryExpression 39 | │ ├──NumberExpression 40 | │ │ └──NumberToken 1 41 | │ ├──PlusToken 42 | │ └──NumberExpression 43 | │ └──NumberToken 2 44 | ├──StarToken 45 | └──NumberExpression 46 | └──NumberToken 3 47 | ``` 48 | 49 | The problem with having incorrect trees is that you interpret results 50 | incorrectly. For instance, when walking the first tree one would compute the 51 | (correct) result `7` while the latter one would compute `9`. 52 | 53 | In our parser (which is a handwritten [recursive descent parser][rdp]) we 54 | achieved this by [structuring our method calls accordingly][parsing]. 55 | 56 | [rdp]: https://en.wikipedia.org/wiki/Recursive_descent_parser 57 | [parsing]: https://github.com/terrajobst/minsk/blob/c6812a81e81611c13ed3a1b1a8b5e802507c95ac/mc/CodeAnalysis/Parser.cs#L74-L102 58 | 59 | ### Fabricating tokens 60 | 61 | In some cases, the parser asserts that specific tokens are present. For example, 62 | when parsing a parenthesized expression, it will assert that after consuming a 63 | `(` and an ``, a `)` token follows. If the current token doesn't match 64 | the expectation, [it will fabricate a token][match] out of thin air. 65 | 66 | This is useful as it avoids cases where later parts of the compiler that walk 67 | the syntax tree have to assume anything could be null. 68 | 69 | [match]: https://github.com/terrajobst/minsk/blob/c6812a81e81611c13ed3a1b1a8b5e802507c95ac/mc/CodeAnalysis/Parser.cs#L53-L60 -------------------------------------------------------------------------------- /docs/episode-05.md: -------------------------------------------------------------------------------- 1 | # Episode 5 2 | 3 | [Video](https://www.youtube.com/watch?v=EEzuO9XWmUY&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=5) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/16) | 5 | [Previous](episode-04.md) | 6 | [Next](episode-06.md) 7 | 8 | ## Completed items 9 | 10 | * A ton of clean-up 11 | * Added `SourceText`, which allows us to compute line number information 12 | 13 | ## Interesting aspects 14 | 15 | ### Positions and Line Numbers 16 | 17 | Our entire frontend is referring to the input as positions, i.e. a zero-based 18 | offset into the text that was parsed. Positions are awesome because you can 19 | easily do math on them. Unfortunately, they aren't great for error reporting. 20 | What you really want is line number and character position. 21 | 22 | We added the concept of [`SourceText`][SourceText] which you can think of as 23 | representing the document the user is editing. It's immutable and it has a 24 | collection of line information. The `SourceText` is stored on the `SyntaxTree` 25 | and can be used to get the index of a line given a position: 26 | 27 | ```C# 28 | var lineIndex = syntaxTree.Text.GetLineIndex(diagnostic.Span.Start); 29 | var line = syntaxTree.Text.Lines[lineIndex]; 30 | var lineNumber = lineIndex + 1; 31 | var character = diagnostic.Span.Start - line.Start + 1; 32 | ``` 33 | 34 | ### Computing line indexes 35 | 36 | `SourceText` has a collection of [`TextLines`][TextLine] which know the start 37 | and end positions for each line. In order to compute a line index, we only 38 | have to [perform a binary search][GetLineIndex]: 39 | 40 | ```C# 41 | public int GetLineIndex(int position) 42 | { 43 | var lower = 0; 44 | var upper = Lines.Length - 1; 45 | 46 | while (lower <= upper) 47 | { 48 | var index = lower + (upper - lower) / 2; 49 | var start = Lines[index].Start; 50 | 51 | if (position == start) 52 | return index; 53 | 54 | if (start > position) 55 | { 56 | upper = index - 1; 57 | } 58 | else 59 | { 60 | lower = index + 1; 61 | } 62 | } 63 | 64 | return lower - 1; 65 | } 66 | ``` 67 | 68 | [SourceText]: https://github.com/terrajobst/minsk/blob/ea1e6f1285cea9ac833d03742cd0c61a3e7c549f/Minsk/CodeAnalysis/Text/SourceText.cs 69 | [TextLine]: https://github.com/terrajobst/minsk/blob/ea1e6f1285cea9ac833d03742cd0c61a3e7c549f/Minsk/CodeAnalysis/Text/TextLine.cs 70 | [GetLineIndex]: https://github.com/terrajobst/minsk/blob/ea1e6f1285cea9ac833d03742cd0c61a3e7c549f/Minsk/CodeAnalysis/Text/SourceText.cs#L22-L46 71 | -------------------------------------------------------------------------------- /docs/episode-03.md: -------------------------------------------------------------------------------- 1 | # Episode 3 2 | 3 | [Video](https://www.youtube.com/watch?v=61dLQNgd9o8&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=3) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/7) | 5 | [Previous](episode-02.md) | 6 | [Next](episode-04.md) 7 | 8 | ## Completed items 9 | 10 | * Extracted compiler into a separate library 11 | * Exposed span on diagnostics that indicate where the error occurred 12 | * Support for assignments and variables 13 | 14 | ## Interesting aspects 15 | 16 | ### Compilation API 17 | 18 | We've added a type called `Compilation` that holds onto the entire state of the 19 | program. It will eventually expose declared symbols as well and house all 20 | compiler operations, such as emitting code. For now, it only exposes an 21 | `Evaluate` API that will interpret the expression: 22 | 23 | ```C# 24 | var syntaxTree = SyntaxTree.Parse(line); 25 | var compilation = new Compilation(syntaxTree); 26 | var result = compilation.Evaluate(); 27 | Console.WriteLine(result.Value); 28 | ``` 29 | 30 | ### Assignments as expressions 31 | 32 | One controversial aspect of the C language family is that assignments are 33 | usually treated as expressions, rather than isolated top-level statements. This 34 | allows writing code like this: 35 | 36 | ```C# 37 | a = b = 5 38 | ``` 39 | 40 | It is tempting to think about assignments as binary operators but they will have 41 | to parse very differently. For instance, consider the parse tree for the 42 | expression `a + b + 5`: 43 | 44 | ``` 45 | + 46 | / \ 47 | + 5 48 | / \ 49 | a b 50 | ``` 51 | 52 | This tree shape isn't desired for assignments. Rather, you'd want: 53 | 54 | ``` 55 | = 56 | / \ 57 | a = 58 | / \ 59 | b 5 60 | ``` 61 | 62 | which means that first `b` is assigned the value `5` and then `a` is assigned 63 | the value `5`. In other words, the `=` is *right associative*. 64 | 65 | Furthermore one needs to decide what the left-hand-side of the assignment 66 | expression can be. It usually is just a variable name, but it could also be a 67 | qualified name or an array index. Thus, most compilers will simply represent it 68 | as an expression. However, not all expressions can be assigned to, for example 69 | the literal `5` couldn't. The ones that can be assigned to, are often referred 70 | to as *L-values* because they can be on the left-hand-side of an assignment. 71 | 72 | In our case, we currently only allow variable names, so we just represent it as 73 | [single token][token], rather than as a general expression. This also makes 74 | parsing them very easy as [can just peek ahead][peek]. 75 | 76 | [token]: https://github.com/terrajobst/minsk/blob/9f5d7b60be92a50ff2618ca0c534ae645c694c65/Minsk/CodeAnalysis/Syntax/AssignmentExpressionSyntax.cs#L15 77 | [peek]: https://github.com/terrajobst/minsk/blob/9f5d7b60be92a50ff2618ca0c534ae645c694c65/Minsk/CodeAnalysis/Syntax/Parser.cs#L74-L86 -------------------------------------------------------------------------------- /docs/episode-02.md: -------------------------------------------------------------------------------- 1 | # Episode 2 2 | 3 | [Video](https://www.youtube.com/watch?v=3XM9vUGduhk&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=3) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/3) | 5 | [Previous](episode-01.md) | 6 | [Next](episode-03.md) 7 | 8 | ## Completed items 9 | 10 | * Generalized parsing using precedences 11 | * Support unary operators, such as `+2` and `-3` 12 | * Support for Boolean literals (`false`, `true`) 13 | * Support for conditions such as `1 == 3 && 2 != 3 || true` 14 | * Internal representation for type checking (`Binder`, and `BoundNode`) 15 | 16 | ## Interesting aspects 17 | 18 | ### Generalized precedence parsing 19 | 20 | In the [first episode](episode-01.md), we've written our recursive descent 21 | parser in such a way that it parses additive and multiplicative expressions 22 | correctly. We did this by parsing `+` and `-` in one method (`ParseTerm`) and 23 | the `*` and `/` operators in another method `ParseFactor`. However, this doesn't 24 | scale very well if you have a dozen operators. In this episode, we've replaced 25 | this with [unified method][precedence-parsing]. 26 | 27 | [precedence-parsing]: https://github.com/terrajobst/minsk/blob/b9e0a3f8858b410ead4afbc3e165c316a628208e/mc/CodeAnalysis/Syntax/Parser.cs#L69-L96 28 | 29 | ### Bound tree 30 | 31 | Our first version of the evaluator was walking the syntax tree directly. But the 32 | syntax tree doesn't have any *semantic* information, for example, it doesn't 33 | know which types an expression will be evaluating to. This makes more 34 | complicated features close to impossible, for instance having operators that 35 | depend on the input types. 36 | 37 | To tackle this, we've introduced the concept of a *bound tree*. The bound tree 38 | is created by the [Binder][binder] by walking the syntax tree and *binding* the 39 | nodes to symbolic information. The binder represents the semantic analysis of 40 | our compiler and will perform things like looking up variable names in scope, 41 | performing type checks, and enforcing correctness rules. 42 | 43 | You can see this in action in [Binder.BindBinaryExpression][bind-binary] which 44 | binds `BinaryExpressionSyntax` to a [BoundBinaryExpression][bound-binary]. The 45 | operator is looked up by using the types of the left and right expressions in 46 | [BoundBinaryOperator.Bind][bind-binary-op]. 47 | 48 | [binder]: https://github.com/terrajobst/minsk/blob/9fa4ecb5347575cd5699afb659074c76f3f2e0fa/mc/CodeAnalysis/Binding/Binder.cs 49 | [bind-binary]: https://github.com/terrajobst/minsk/blob/9fa4ecb5347575cd5699afb659074c76f3f2e0fa/mc/CodeAnalysis/Binding/Binder.cs#L48-L60 50 | [bound-binary]: https://github.com/terrajobst/minsk/blob/9fa4ecb5347575cd5699afb659074c76f3f2e0fa/mc/CodeAnalysis/Binding/BoundBinaryExpression.cs#L5-L18 51 | [bind-binary-op]: https://github.com/terrajobst/minsk/blob/9fa4ecb5347575cd5699afb659074c76f3f2e0fa/mc/CodeAnalysis/Binding/BoundBinaryOperator.cs#L50-L59 52 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | # Force bash scripts to always use lf line endings so that if a repro is accessed 66 | # in Unix via a file share from Windows, the scripts will work. 67 | *.sh text eol=lf 68 | -------------------------------------------------------------------------------- /docs/episode-19.md: -------------------------------------------------------------------------------- 1 | # Episode 19 2 | 3 | [Video](https://www.youtube.com/watch?v=Ecrv8sCYEbA&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=19) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/96) | 5 | [Previous](episode-18.md) | 6 | [Next](episode-20.md) 7 | 8 | ## Completed items 9 | 10 | * Replaced hard-coded IL emitter for Hello World by one that uses our 11 | intermediate representation 12 | * Emit `input()` and variables 13 | * Emit `string` concatenation 14 | * Emit assignments 15 | * Emit non-`void` functions and parameters 16 | 17 | ## Interesting aspects 18 | 19 | ### Encoding of IL instructions 20 | 21 | IL instructions are ultimately bytes. Most instruction sets try to optimize for 22 | size while also making sure that the format is extensible and flexible. IL is no 23 | different. Instructions come in two forms, by themselves or with a parameter, 24 | which is called an intermediate. The intermediate is used as an additional 25 | parameter for the instruction, for example, the local variable being stored, the 26 | method being called or the literal being loaded. IL only allows for a single 27 | intermediate, which is why many instructions don't arguments directly but 28 | instead use the evaluation stack. For example, the `add` instruction doesn't 29 | take two arguments but instead takes them from the stack. 30 | 31 | The general instruction for loading 32-bit integer values is `ldc.i4`. The 32 | intermediate is the value, for example `ldc.i4 42` loads the value `42`. Since 33 | some values are extremely common in programs (such as `0` and `1`) there are 34 | special instructions who don't need an intermediate because the instruction 35 | itself represents the value being loaded, for example `ldc.i4.0` just loads the 36 | value `0`. This reduces the size of IL. 37 | 38 | As a compiler writer, dealing with these special encodings can be tedious but 39 | fortunately we don't have to. We're using `Mono.Cecil` for emitting IL and it 40 | has the handy `body.OptimizeMacros()` method which will replace instructions 41 | accordingly. 42 | 43 | ### Booleans in IL 44 | 45 | While `System.Boolean` is a type, IL itself has no instructions that deal with 46 | Booleans. Instead, a Boolean is just a 32-bit integer. There are no instructions 47 | to load the values `true` and `false`. Instead, we can use `ldc.i4.0` (`false`) 48 | and `ldc.i4.1` (`true`). 49 | 50 | ### Local variables and parameters in IL 51 | 52 | In IL, locals and parameters aren't referred to by name. In fact, the metadata 53 | format doesn't even record the names of locals (it is, however, part of the 54 | debugging information). 55 | 56 | They are referred to by index. For example `ldloc.0` loads the first local while 57 | `stloc.2` writes to the third. 58 | 59 | Instance methods have an implied parameter that refers to the instance, which in 60 | C# is available via the `this` keyword. In IL, it's simply the first parameter, 61 | `ldarg.0`. In static methods, `ldarg.0` is still valid, but it will refer to the 62 | first actual parameter. 63 | -------------------------------------------------------------------------------- /docs/episode-13.md: -------------------------------------------------------------------------------- 1 | # Episode 13 2 | 3 | [Video](https://www.youtube.com/watch?v=NvVc8erZpeI&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=13) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/61) | 5 | [Previous](episode-12.md) | 6 | [Next](episode-14.md) 7 | 8 | ## Completed items 9 | 10 | We added pretty printing for bound nodes as well as `break` and `continue` 11 | statements. 12 | 13 | ## Interesting aspects 14 | 15 | ### Break and continue 16 | 17 | Logically, all `if` statements and loops are basically just `goto`-statements. 18 | In order to support `break` and `continue`, we only have to make sure that all 19 | [loops have predefined labels][bound-loop] for `break` and `continue`. That 20 | means that we can just bind them to a `BoundGotoStatement`. 21 | 22 | During binding, we only have to track the current loop by using a 23 | [stack][loop-stack] that has a tuple of `break` and `continue` labels. When 24 | binding a loop body, we [generate][bind-loop-body] labels for `break` and 25 | `continue` and push them onto that stack. And for [binding `break` and 26 | `continue`][bind-break-continue], we only have to use the corresponding label 27 | from the stack. 28 | 29 | [bound-loop]: https://github.com/terrajobst/minsk/blob/3982452187b615acd60db8ec2d26a3b0cf924c44/src/Minsk/CodeAnalysis/Binding/BoundLoopStatement.cs#L11-L12 30 | [loop-stack]: https://github.com/terrajobst/minsk/blob/3982452187b615acd60db8ec2d26a3b0cf924c44/src/Minsk/CodeAnalysis/Binding/Binder.cs#L17 31 | [bind-loop-body]: https://github.com/terrajobst/minsk/blob/3982452187b615acd60db8ec2d26a3b0cf924c44/src/Minsk/CodeAnalysis/Binding/Binder.cs#L268-L279 32 | [bind-break-continue]: https://github.com/terrajobst/minsk/blob/3982452187b615acd60db8ec2d26a3b0cf924c44/src/Minsk/CodeAnalysis/Binding/Binder.cs#L281-L303 33 | 34 | ### Binder state 35 | 36 | The current design of the binder has mutable state. The assumption is that the 37 | binder is only used in one of two cases: 38 | 39 | 1. [Binding global scope][bind-global-scope]. Since we want to allow developers 40 | to declare functions in any order, we first need to bind the global scope, 41 | that is declare all global variables and functions. Modulo diagnostics, this 42 | requires no state. 43 | 44 | 2. [Binding function bodies][bind-function-body]. Given the bound global scope, 45 | we then create a binder per function body for binding. This means the state 46 | on the binder can assume that all its state is for the current function. In 47 | other words, we don't have to worry that our loop stack would allow one 48 | function to accidentally transfer control to a statement in another function. 49 | 50 | This separation also makes it easy to parallelize the compiler. For example, we 51 | could bind all function bodies in parallel. 52 | 53 | [bind-global-scope]: https://github.com/terrajobst/minsk/blob/3982452187b615acd60db8ec2d26a3b0cf924c44/src/Minsk/CodeAnalysis/Binding/Binder.cs#L33-L57 54 | [bind-function-body]: https://github.com/terrajobst/minsk/blob/3982452187b615acd60db8ec2d26a3b0cf924c44/src/Minsk/CodeAnalysis/Binding/Binder.cs#L72-L73 -------------------------------------------------------------------------------- /src/minsk.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.30011.22 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minsk", "Minsk\Minsk.csproj", "{6FE6F7CC-1626-4731-9B68-7236B5A4C94C}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minsk.Generators", "Minsk.Generators\Minsk.Generators.csproj", "{C106F0D5-AEA3-460F-AA42-B796C6F85B3D}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minsk.Tests", "Minsk.Tests\Minsk.Tests.csproj", "{36E2CC3E-B08D-4B6D-8901-651C800CABAC}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "msc", "msc\msc.csproj", "{016A06A3-0FED-42BE-99CA-74FDE1D0D03D}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "msi", "msi\msi.csproj", "{9C7AFA59-BB65-46AB-A14B-30F601CBDC8C}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {6FE6F7CC-1626-4731-9B68-7236B5A4C94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {6FE6F7CC-1626-4731-9B68-7236B5A4C94C}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {6FE6F7CC-1626-4731-9B68-7236B5A4C94C}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {6FE6F7CC-1626-4731-9B68-7236B5A4C94C}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {C106F0D5-AEA3-460F-AA42-B796C6F85B3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {C106F0D5-AEA3-460F-AA42-B796C6F85B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {C106F0D5-AEA3-460F-AA42-B796C6F85B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {C106F0D5-AEA3-460F-AA42-B796C6F85B3D}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {36E2CC3E-B08D-4B6D-8901-651C800CABAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {36E2CC3E-B08D-4B6D-8901-651C800CABAC}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {36E2CC3E-B08D-4B6D-8901-651C800CABAC}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {36E2CC3E-B08D-4B6D-8901-651C800CABAC}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {016A06A3-0FED-42BE-99CA-74FDE1D0D03D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {016A06A3-0FED-42BE-99CA-74FDE1D0D03D}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {016A06A3-0FED-42BE-99CA-74FDE1D0D03D}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {016A06A3-0FED-42BE-99CA-74FDE1D0D03D}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {9C7AFA59-BB65-46AB-A14B-30F601CBDC8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {9C7AFA59-BB65-46AB-A14B-30F601CBDC8C}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {9C7AFA59-BB65-46AB-A14B-30F601CBDC8C}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {9C7AFA59-BB65-46AB-A14B-30F601CBDC8C}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {A452DD96-3811-44D9-8F7F-B9DDAAA8ED40} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/msc/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Minsk.CodeAnalysis; 6 | using Minsk.CodeAnalysis.Syntax; 7 | using Minsk.IO; 8 | using Mono.Options; 9 | 10 | namespace Minsk 11 | { 12 | internal static class Program 13 | { 14 | private static int Main(string[] args) 15 | { 16 | var outputPath = (string?) null; 17 | var moduleName = (string?) null; 18 | var referencePaths = new List(); 19 | var sourcePaths = new List(); 20 | var helpRequested = false; 21 | 22 | var options = new OptionSet 23 | { 24 | "usage: msc [options]", 25 | { "r=", "The {path} of an assembly to reference", v => referencePaths.Add(v) }, 26 | { "o=", "The output {path} of the assembly to create", v => outputPath = v }, 27 | { "m=", "The {name} of the module", v => moduleName = v }, 28 | { "?|h|help", "Prints help", v => helpRequested = true }, 29 | { "<>", v => sourcePaths.Add(v) } 30 | }; 31 | 32 | options.Parse(args); 33 | 34 | if (helpRequested) 35 | { 36 | options.WriteOptionDescriptions(Console.Out); 37 | return 0; 38 | } 39 | 40 | if (sourcePaths.Count == 0) 41 | { 42 | Console.Error.WriteLine("error: need at least one source file"); 43 | return 1; 44 | } 45 | 46 | if (outputPath == null) 47 | outputPath = Path.ChangeExtension(sourcePaths[0], ".exe"); 48 | 49 | if (moduleName == null) 50 | moduleName = Path.GetFileNameWithoutExtension(outputPath); 51 | 52 | var syntaxTrees = new List(); 53 | var hasErrors = false; 54 | 55 | foreach (var path in sourcePaths) 56 | { 57 | if (!File.Exists(path)) 58 | { 59 | Console.Error.WriteLine($"error: file '{path}' doesn't exist"); 60 | hasErrors = true; 61 | continue; 62 | } 63 | 64 | var syntaxTree = SyntaxTree.Load(path); 65 | syntaxTrees.Add(syntaxTree); 66 | } 67 | 68 | foreach (var path in referencePaths) 69 | { 70 | if (!File.Exists(path)) 71 | { 72 | Console.Error.WriteLine($"error: file '{path}' doesn't exist"); 73 | hasErrors = true; 74 | continue; 75 | } 76 | } 77 | 78 | if (hasErrors) 79 | return 1; 80 | 81 | var compilation = Compilation.Create(syntaxTrees.ToArray()); 82 | var diagnostics = compilation.Emit(moduleName, referencePaths.ToArray(), outputPath); 83 | 84 | if (diagnostics.Any()) 85 | { 86 | Console.Error.WriteDiagnostics(diagnostics); 87 | return 1; 88 | } 89 | 90 | return 0; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/msi/Authoring/Classifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | 4 | using Minsk.CodeAnalysis.Syntax; 5 | using Minsk.CodeAnalysis.Text; 6 | 7 | namespace Minsk.CodeAnalysis.Authoring 8 | { 9 | public static class Classifier 10 | { 11 | public static ImmutableArray Classify(SyntaxTree syntaxTree, TextSpan span) 12 | { 13 | var result = ImmutableArray.CreateBuilder(); 14 | ClassifyNode(syntaxTree.Root, span, result); 15 | return result.ToImmutable(); 16 | } 17 | 18 | private static void ClassifyNode(SyntaxNode node, TextSpan span, ImmutableArray.Builder result) 19 | { 20 | if (!node.FullSpan.OverlapsWith(span)) 21 | return; 22 | 23 | if (node is SyntaxToken token) 24 | ClassifyToken(token, span, result); 25 | 26 | foreach (var child in node.GetChildren()) 27 | ClassifyNode(child, span, result); 28 | } 29 | 30 | private static void ClassifyToken(SyntaxToken token, TextSpan span, ImmutableArray.Builder result) 31 | { 32 | foreach (var leadingTrivia in token.LeadingTrivia) 33 | ClassifyTrivia(leadingTrivia, span, result); 34 | 35 | AddClassification(token.Kind, token.Span, span, result); 36 | 37 | foreach (var trailingTrivia in token.TrailingTrivia) 38 | ClassifyTrivia(trailingTrivia, span, result); 39 | } 40 | 41 | private static void ClassifyTrivia(SyntaxTrivia trivia, TextSpan span, ImmutableArray.Builder result) 42 | { 43 | AddClassification(trivia.Kind, trivia.Span, span, result); 44 | } 45 | 46 | private static void AddClassification(SyntaxKind elementKind, TextSpan elementSpan, TextSpan span, ImmutableArray.Builder result) 47 | { 48 | if (!elementSpan.OverlapsWith(span)) 49 | return; 50 | 51 | var adjustedStart = Math.Max(elementSpan.Start, span.Start); 52 | var adjustedEnd = Math.Min(elementSpan.End, span.End); 53 | var adjustedSpan = TextSpan.FromBounds(adjustedStart, adjustedEnd); 54 | var classification = GetClassification(elementKind); 55 | 56 | var classifiedSpan = new ClassifiedSpan(adjustedSpan, classification); 57 | result.Add(classifiedSpan); 58 | } 59 | 60 | private static Classification GetClassification(SyntaxKind kind) 61 | { 62 | var isKeyword = kind.IsKeyword(); 63 | var isIdentifier = kind == SyntaxKind.IdentifierToken; 64 | var isNumber = kind == SyntaxKind.NumberToken; 65 | var isString = kind == SyntaxKind.StringToken; 66 | var isComment = kind.IsComment(); 67 | 68 | if (isKeyword) 69 | return Classification.Keyword; 70 | else if (isIdentifier) 71 | return Classification.Identifier; 72 | else if (isNumber) 73 | return Classification.Number; 74 | else if (isString) 75 | return Classification.String; 76 | else if (isComment) 77 | return Classification.Comment; 78 | else 79 | return Classification.Text; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/episode-17.md: -------------------------------------------------------------------------------- 1 | # Episode 17 2 | 3 | [Video](https://www.youtube.com/watch?v=Lsi1Itrzyl4&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=17) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/89) | 5 | [Previous](episode-16.md) | 6 | [Next](episode-18.md) 7 | 8 | ## Completed items 9 | 10 | * Introduce `Compilation.IsScript` and use it to restrict expression statements 11 | * Support implicit argument conversions when calling functions 12 | * Add `any` type 13 | * Lower global statements into `main` function 14 | 15 | ## Interesting aspects 16 | 17 | ### Regular vs. script mode 18 | 19 | In virtual all C-like languages some expressions are also allowed as statements. 20 | The canonical examples are assignments and expressions: 21 | 22 | ```JavaScript 23 | x = 10 24 | print(string(x)) 25 | ``` 26 | 27 | Syntactically, this also allows for other expressions such as 28 | 29 | ```JavaScript 30 | x + 1 31 | ``` 32 | 33 | Normally these expressions are pointless because their values aren't observed. 34 | Strictly speaking these expressions aren't pure, for instance `f()` could have a 35 | side effect here: 36 | 37 | ```JavaScript 38 | x + f(3) 39 | ``` 40 | 41 | But the top level binary expression will produce a value that's not going 42 | anywhere, which is most likely indicative that the developer made a mistake. 43 | Hence, most C-like languages disallow or at least warn when they encounter these 44 | expressions. 45 | 46 | However, when entering code in a REPL these expression are super useful. And 47 | their return value is observed by printing it back to the console. 48 | 49 | To differentiate between the two modes we're changing our `Compilation` to be in 50 | either script- or in regular mode: 51 | 52 | * **regular mode** will only allow *assignment-* and *call expressions* inside 53 | of expression statements while 54 | 55 | * **script mode** will allow any expression so long the containing statement is 56 | a global statement (in other words as soon as the statement is part of a block 57 | it's like in regular mode). 58 | 59 | ### Lowering global statements 60 | 61 | We'd like our logical model to be that all code is contained in a function. For 62 | regular programs that are compiled that means we're expected to have a `main` 63 | function where execution begins. `main` takes no arguments and returns no value 64 | (for simplicity, we can change that later). 65 | 66 | In script mode, we want a script function that takes no arguments and returns 67 | `any` (that is an expression of any type, like `object` in C#). 68 | 69 | For ease of use we'll still allow global statements in our language which means 70 | we're ending with these modes: 71 | 72 | * **Regular mode**. The developer can use global statements or explicitly 73 | declare a `main` function. When global statements are used, the compiler will 74 | synthesize a `main` function that will contain those statements. That's why 75 | using both global statements and a `main` function is illegal. Furthermore, we 76 | only allow one syntax tree to have global statements because unless we allow 77 | the developer to control the order of files, the execution order between them 78 | would be ill-defined. 79 | 80 | * **Script mode**. The developer can declare a function called `main` in script 81 | mode but the function isn't treated specially and thus doesn't conflict with 82 | global statements. When global statements are used, they are put in a 83 | synthesized function with a name that the developer can't use (this avoids 84 | naming conflicts). 85 | 86 | That means regardless of form, both models end up with a collection of functions 87 | and no global statements. Having this unified shape will make it easier to 88 | generate code later. 89 | -------------------------------------------------------------------------------- /src/Minsk.Tests/CodeAnalysis/AnnotatedText.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Text; 6 | using Minsk.CodeAnalysis.Text; 7 | 8 | namespace Minsk.Tests.CodeAnalysis 9 | { 10 | internal sealed class AnnotatedText 11 | { 12 | public AnnotatedText(string text, ImmutableArray spans) 13 | { 14 | Text = text; 15 | Spans = spans; 16 | } 17 | 18 | public string Text { get; } 19 | public ImmutableArray Spans { get; } 20 | 21 | public static AnnotatedText Parse(string text) 22 | { 23 | text = Unindent(text); 24 | 25 | var textBuilder = new StringBuilder(); 26 | var spanBuilder = ImmutableArray.CreateBuilder(); 27 | var startStack = new Stack(); 28 | 29 | var position = 0; 30 | 31 | foreach (var c in text) 32 | { 33 | if (c == '[') 34 | { 35 | startStack.Push(position); 36 | } 37 | else if (c == ']') 38 | { 39 | if (startStack.Count == 0) 40 | throw new ArgumentException("Too many ']' in text", nameof(text)); 41 | 42 | var start = startStack.Pop(); 43 | var end = position; 44 | var span = TextSpan.FromBounds(start, end); 45 | spanBuilder.Add(span); 46 | } 47 | else 48 | { 49 | position++; 50 | textBuilder.Append(c); 51 | } 52 | } 53 | 54 | if (startStack.Count != 0) 55 | throw new ArgumentException("Missing ']' in text", nameof(text)); 56 | 57 | return new AnnotatedText(textBuilder.ToString(), spanBuilder.ToImmutable()); 58 | } 59 | 60 | private static string Unindent(string text) 61 | { 62 | var lines = UnindentLines(text); 63 | return string.Join(Environment.NewLine, lines); 64 | } 65 | 66 | public static string[] UnindentLines(string text) 67 | { 68 | var lines = new List(); 69 | 70 | using (var reader = new StringReader(text)) 71 | { 72 | string? line; 73 | while ((line = reader.ReadLine()) != null) 74 | lines.Add(line); 75 | } 76 | 77 | var minIndentation = int.MaxValue; 78 | for (var i = 0; i < lines.Count; i++) 79 | { 80 | var line = lines[i]; 81 | 82 | if (line.Trim().Length == 0) 83 | { 84 | lines[i] = string.Empty; 85 | continue; 86 | } 87 | 88 | var indentation = line.Length - line.TrimStart().Length; 89 | minIndentation = Math.Min(minIndentation, indentation); 90 | } 91 | 92 | for (var i = 0; i < lines.Count; i++) 93 | { 94 | if (lines[i].Length == 0) 95 | continue; 96 | 97 | lines[i] = lines[i].Substring(minIndentation); 98 | } 99 | 100 | while (lines.Count > 0 && lines[0].Length == 0) 101 | lines.RemoveAt(0); 102 | 103 | while (lines.Count > 0 && lines[lines.Count - 1].Length == 0) 104 | lines.RemoveAt(lines.Count - 1); 105 | 106 | return lines.ToArray(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Symbols/SymbolPrinter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Minsk.CodeAnalysis.Syntax; 4 | using Minsk.IO; 5 | 6 | namespace Minsk.CodeAnalysis.Symbols 7 | { 8 | internal static class SymbolPrinter 9 | { 10 | public static void WriteTo(Symbol symbol, TextWriter writer) 11 | { 12 | switch (symbol.Kind) 13 | { 14 | case SymbolKind.Function: 15 | WriteFunctionTo((FunctionSymbol)symbol, writer); 16 | break; 17 | case SymbolKind.GlobalVariable: 18 | WriteGlobalVariableTo((GlobalVariableSymbol)symbol, writer); 19 | break; 20 | case SymbolKind.LocalVariable: 21 | WriteLocalVariableTo((LocalVariableSymbol)symbol, writer); 22 | break; 23 | case SymbolKind.Parameter: 24 | WriteParameterTo((ParameterSymbol)symbol, writer); 25 | break; 26 | case SymbolKind.Type: 27 | WriteTypeTo((TypeSymbol)symbol, writer); 28 | break; 29 | default: 30 | throw new Exception($"Unexpected symbol: {symbol.Kind}"); 31 | } 32 | } 33 | 34 | private static void WriteFunctionTo(FunctionSymbol symbol, TextWriter writer) 35 | { 36 | writer.WriteKeyword(SyntaxKind.FunctionKeyword); 37 | writer.WriteSpace(); 38 | writer.WriteIdentifier(symbol.Name); 39 | writer.WritePunctuation(SyntaxKind.OpenParenthesisToken); 40 | 41 | for (int i = 0; i < symbol.Parameters.Length; i++) 42 | { 43 | if (i > 0) 44 | { 45 | writer.WritePunctuation(SyntaxKind.CommaToken); 46 | writer.WriteSpace(); 47 | } 48 | 49 | symbol.Parameters[i].WriteTo(writer); 50 | } 51 | 52 | writer.WritePunctuation(SyntaxKind.CloseParenthesisToken); 53 | 54 | if (symbol.Type != TypeSymbol.Void) 55 | { 56 | writer.WritePunctuation(SyntaxKind.ColonToken); 57 | writer.WriteSpace(); 58 | symbol.Type.WriteTo(writer); 59 | } 60 | } 61 | 62 | private static void WriteGlobalVariableTo(GlobalVariableSymbol symbol, TextWriter writer) 63 | { 64 | writer.WriteKeyword(symbol.IsReadOnly ? SyntaxKind.LetKeyword : SyntaxKind.VarKeyword); 65 | writer.WriteSpace(); 66 | writer.WriteIdentifier(symbol.Name); 67 | writer.WritePunctuation(SyntaxKind.ColonToken); 68 | writer.WriteSpace(); 69 | symbol.Type.WriteTo(writer); 70 | } 71 | 72 | private static void WriteLocalVariableTo(LocalVariableSymbol symbol, TextWriter writer) 73 | { 74 | writer.WriteKeyword(symbol.IsReadOnly ? SyntaxKind.LetKeyword : SyntaxKind.VarKeyword); 75 | writer.WriteSpace(); 76 | writer.WriteIdentifier(symbol.Name); 77 | writer.WritePunctuation(SyntaxKind.ColonToken); 78 | writer.WriteSpace(); 79 | symbol.Type.WriteTo(writer); 80 | } 81 | 82 | private static void WriteParameterTo(ParameterSymbol symbol, TextWriter writer) 83 | { 84 | writer.WriteIdentifier(symbol.Name); 85 | writer.WritePunctuation(SyntaxKind.ColonToken); 86 | writer.WriteSpace(); 87 | symbol.Type.WriteTo(writer); 88 | } 89 | 90 | private static void WriteTypeTo(TypeSymbol symbol, TextWriter writer) 91 | { 92 | writer.WriteIdentifier(symbol.Name); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Text/SourceText.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Minsk.CodeAnalysis.Text 4 | { 5 | public sealed class SourceText 6 | { 7 | private readonly string _text; 8 | 9 | private SourceText(string text, string fileName) 10 | { 11 | _text = text; 12 | FileName = fileName; 13 | Lines = ParseLines(this, text); 14 | } 15 | 16 | public static SourceText From(string text, string fileName = "") 17 | { 18 | return new SourceText(text, fileName); 19 | } 20 | 21 | private static ImmutableArray ParseLines(SourceText sourceText, string text) 22 | { 23 | var result = ImmutableArray.CreateBuilder(); 24 | 25 | var position = 0; 26 | var lineStart = 0; 27 | 28 | while (position < text.Length) 29 | { 30 | var lineBreakWidth = GetLineBreakWidth(text, position); 31 | 32 | if (lineBreakWidth == 0) 33 | { 34 | position++; 35 | } 36 | else 37 | { 38 | AddLine(result, sourceText, position, lineStart, lineBreakWidth); 39 | 40 | position += lineBreakWidth; 41 | lineStart = position; 42 | } 43 | } 44 | 45 | if (position >= lineStart) 46 | AddLine(result, sourceText, position, lineStart, 0); 47 | 48 | return result.ToImmutable(); 49 | } 50 | 51 | private static void AddLine(ImmutableArray.Builder result, SourceText sourceText, int position, int lineStart, int lineBreakWidth) 52 | { 53 | var lineLength = position - lineStart; 54 | var lineLengthIncludingLineBreak = lineLength + lineBreakWidth; 55 | var line = new TextLine(sourceText, lineStart, lineLength, lineLengthIncludingLineBreak); 56 | result.Add(line); 57 | } 58 | 59 | private static int GetLineBreakWidth(string text, int position) 60 | { 61 | var c = text[position]; 62 | var l = position + 1 >= text.Length ? '\0' : text[position + 1]; 63 | 64 | if (c == '\r' && l == '\n') 65 | return 2; 66 | 67 | if (c == '\r' || c == '\n') 68 | return 1; 69 | 70 | return 0; 71 | } 72 | 73 | public ImmutableArray Lines { get; } 74 | 75 | public char this[int index] => _text[index]; 76 | 77 | public int Length => _text.Length; 78 | 79 | public string FileName { get; } 80 | 81 | public int GetLineIndex(int position) 82 | { 83 | var lower = 0; 84 | var upper = Lines.Length - 1; 85 | 86 | while (lower <= upper) 87 | { 88 | var index = lower + (upper - lower) / 2; 89 | var start = Lines[index].Start; 90 | 91 | if (position == start) 92 | return index; 93 | 94 | if (start > position) 95 | { 96 | upper = index - 1; 97 | } 98 | else 99 | { 100 | lower = index + 1; 101 | } 102 | } 103 | 104 | return lower - 1; 105 | } 106 | 107 | public override string ToString() => _text; 108 | 109 | public string ToString(int start, int length) => _text.Substring(start, length); 110 | 111 | public string ToString(TextSpan span) => ToString(span.Start, span.Length); 112 | } 113 | } -------------------------------------------------------------------------------- /docs/episode-23.md: -------------------------------------------------------------------------------- 1 | # Episode 23 2 | 3 | [Video](https://www.youtube.com/watch?v=bpVFmD_JYuU&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=24) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/130/files) | 5 | [Previous](episode-22.md) | 6 | [Next](episode-24.md) 7 | 8 | ## Completed items 9 | 10 | * Added single-line and multi-line comments 11 | * Made sure multi-line comments are rendered properly in the REPL 12 | 13 | ## Interesting aspects 14 | 15 | ### Comments are nice 16 | 17 | If you think to yourself "why does he think that's worth mentioning" consider 18 | that JSON doesn't have comments :-) 19 | 20 | Also, while we shamelessly borrowed the comments from the C family, it's worth 21 | mentioning what these choices mean: 22 | 23 | 1. **Comments are valid everywhere where whitespace is valid**. Again, you might 24 | be tempted to say "duh", but consider that's not the case in all languages. 25 | For example, in XML comments are only valid where elements are valid. In 26 | other words, you can't comment out individual attributes. 27 | 2. **We support both single-line and multi-line comments**. Single line comments 28 | are nice because you don't have to write a terminator nor do you have to 29 | worry about escaping them. 30 | 3. **We don't support nested multi-line comments**. For example, `/* /* */ */` 31 | isn't valid. The comment is terminated by the first `*/` which causes a 32 | syntax error for the second one. 33 | 34 | ### Line-independent syntax highlighting 35 | 36 | Right now, we don't have any tokens that span multiple lines. This makes syntax 37 | highlighting rather simple: we can tokenize each line independently. With the 38 | advent of multi-line comments this is no longer the case. Consider I have 39 | several lines of code. Now let's say I insert a new line at the beginning and 40 | start typing `/*`. We now have to repaint multiple lines because they are all 41 | considered being part of the comment. 42 | 43 | A common trick that fast syntax highlighters use is that they will track a state 44 | per line. Usually that state is just an integer representing the initial state 45 | the tokenizer should be considered in when tokenizing the next line. In our case 46 | there would be only two: regular state and an in-comment state. 47 | 48 | We're doing a simpler version of that and simply say that [the state] is the 49 | same for all lines: the fully tokenized input. 50 | 51 | ```C# 52 | private sealed class RenderState 53 | { 54 | public RenderState(SourceText text, ImmutableArray tokens) 55 | { 56 | Text = text; 57 | Tokens = tokens; 58 | } 59 | 60 | public SourceText Text { get; } 61 | public ImmutableArray Tokens { get; } 62 | } 63 | ``` 64 | 65 | When rendering, [we pass the array of lines, the index of the current line, and 66 | the previous line's state][RenderLine]. If the previous line's render state is 67 | `null`, we tokenize the input: 68 | 69 | ```C# 70 | protected override object RenderLine(IReadOnlyList lines, int lineIndex, object state) 71 | { 72 | RenderState renderState; 73 | 74 | if (state == null) 75 | { 76 | var text = string.Join(Environment.NewLine, lines); 77 | var sourceText = SourceText.From(text); 78 | var tokens = SyntaxTree.ParseTokens(sourceText); 79 | renderState = new RenderState(sourceText, tokens); 80 | } 81 | else 82 | { 83 | renderState = (RenderState) state; 84 | } 85 | 86 | // ... 87 | } 88 | ``` 89 | 90 | For the actual rendering we only look at tokens that [overlap with the span of 91 | the current line][tokenLoop]. We also have to make sure that we trim the token 92 | to the line start and line end: 93 | 94 | ```C# 95 | var lineSpan = renderState.Text.Lines[lineIndex].Span; 96 | 97 | foreach (var token in renderState.Tokens) 98 | { 99 | if (!lineSpan.OverlapsWith(token.Span)) 100 | continue; 101 | 102 | var tokenStart = Math.Max(token.Span.Start, lineSpan.Start); 103 | var tokenEnd = Math.Min(token.Span.End, lineSpan.End); 104 | var tokenSpan = TextSpan.FromBounds(tokenStart, tokenEnd); 105 | var tokenText = renderState.Text.ToString(tokenSpan); 106 | 107 | // Print token 108 | } 109 | ``` 110 | 111 | And that's it. 112 | 113 | [the state]: https://github.com/terrajobst/minsk/blob/521bdcd435b813b7b43bd9161ac5041fdc2c8f66/src/msi/MinskRepl.cs#L28-L38 114 | [RenderLine]: https://github.com/terrajobst/minsk/blob/521bdcd435b813b7b43bd9161ac5041fdc2c8f66/src/msi/MinskRepl.cs#L40-L54 115 | [tokenLoop]: https://github.com/terrajobst/minsk/blob/521bdcd435b813b7b43bd9161ac5041fdc2c8f66/src/msi/MinskRepl.cs#L56-L90 -------------------------------------------------------------------------------- /docs/episode-14.md: -------------------------------------------------------------------------------- 1 | # Episode 14 2 | 3 | [Video](https://www.youtube.com/watch?v=5813y1T8lhc&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=14) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/66) | 5 | [Previous](episode-13.md) | 6 | [Next](episode-15.md) 7 | 8 | ## Completed items 9 | 10 | We added support for `return` statements and control flow analysis. 11 | 12 | ## Interesting aspects 13 | 14 | ### To parse or not to parse 15 | 16 | In case of functions without a return type (i.e. procedures), the `return` 17 | keyword can be used to exit it. In case of functions, the `return` keyword 18 | must be followed with an expression. So syntactically both of these forms 19 | are valid: 20 | 21 | ``` 22 | return 23 | return 1 * 2 24 | ``` 25 | 26 | This begs the question if after seeing a `return` keyword an expression needs to 27 | be parsed. 28 | 29 | In a language that has a token that terminates statements (such as the semicolon 30 | in C-based languages) it's pretty straight forward: after seeing the `return` 31 | keyword, an expression is parsed unless the next token is a semicolon. That's 32 | what [C# does too][roslyn-return]. 33 | 34 | But our language doesn't have semicolons. So what can we do? You might think we 35 | could make the parser smarter by trying to parse an expression, but this would 36 | still be ill-defined. For example, what should happen in this case: 37 | 38 | ``` 39 | return 40 | someFunc() 41 | ``` 42 | 43 | Is `someFunc()` supposed to be the return expression? 44 | 45 | I decided to go down the (arguably problematic) path JavaScript took: if the 46 | next token is on the same line, we [parse an expression][parse-return]. 47 | Otherwise, we don't. 48 | 49 | [roslyn-return]: https://github.com/dotnet/roslyn/blob/b5cd612b741668145ad50bb4329a4de94af48490/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs#L7946-L7949 50 | [parse-return]: https://github.com/terrajobst/minsk/blob/a82c3f875802f82b40f933460f767da00449cae2/src/Minsk/CodeAnalysis/Syntax/Parser.cs#L301-L310 51 | 52 | ### Returning is simple but validation is hard 53 | 54 | Implementing the `return` keyword is [pretty straight forward][return-commit]. 55 | What's harder is to decide whether all control flows through a function end in a 56 | return statement. 57 | 58 | You might think this can be done by walking backwards through the statments, but 59 | it's not that easy. Consider this code: 60 | 61 | ```typeScript 62 | function sum(n: int): int 63 | { 64 | var i = 0 65 | var result = 0 66 | while true 67 | { 68 | if (i == n) return result 69 | result = result + i 70 | i = i + 1 71 | } 72 | var z = 0 73 | } 74 | ``` 75 | The statement `var z = 0` isn't followed by a return statement. However, all 76 | flows through the function end up in returning a value -- this statement is 77 | simply unreachable. 78 | 79 | How do we know this? The approach is called [control flow 80 | analysis][control-flow]. The idea is that we create a graph that represents the 81 | control flow of the function. For our example it roughly looks like this: 82 | 83 | ![](episode-14-cfg.svg) 84 | 85 | All nodes in the graph are called [basic blocks][basic-block]. A basic block is 86 | a list of statements that are executed in sequence without any jumps. Only the 87 | first statement in a basic block can be jumped to and only the last statement 88 | can transfer control to other blocks. All edges in this graph represent branches 89 | in control flow. 90 | 91 | All control flow graphs have a single `` and a single `` node. Thus, 92 | empty functions would have two nodes. 93 | 94 | To check wether a function always returns a value, we only have to start at the 95 | `` node and check whether all incoming blocks end with a `return 96 | statement`, ignoring blocks that are unreachable. A node is considered 97 | unreachable if it doesn't have any incoming nodes or all incoming nodes are also 98 | considered unreachable. 99 | 100 | To simplify our lives, we [remove all unreachable nodes][remove-unreachable] so 101 | that [checking returns][check-returns] doesn't have to exclude them. 102 | 103 | [return-commit]: https://github.com/terrajobst/minsk/commit/26d79f8f1e0b30a45405daa25ca0230642de2fa9 104 | [control-flow]: https://en.wikipedia.org/wiki/Control_flow 105 | [basic-block]: https://en.wikipedia.org/wiki/Basic_block 106 | [remove-unreachable]: https://github.com/terrajobst/minsk/blob/a82c3f875802f82b40f933460f767da00449cae2/src/Minsk/CodeAnalysis/Binding/ControlFlowGraph.cs#L201-L209 107 | [check-returns]: https://github.com/terrajobst/minsk/blob/a82c3f875802f82b40f933460f767da00449cae2/src/Minsk/CodeAnalysis/Binding/ControlFlowGraph.cs#L308-L320 108 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundBinaryOperator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | using Minsk.CodeAnalysis.Syntax; 4 | 5 | namespace Minsk.CodeAnalysis.Binding 6 | { 7 | internal sealed class BoundBinaryOperator 8 | { 9 | private BoundBinaryOperator(SyntaxKind syntaxKind, BoundBinaryOperatorKind kind, TypeSymbol type) 10 | : this(syntaxKind, kind, type, type, type) 11 | { 12 | } 13 | 14 | private BoundBinaryOperator(SyntaxKind syntaxKind, BoundBinaryOperatorKind kind, TypeSymbol operandType, TypeSymbol resultType) 15 | : this(syntaxKind, kind, operandType, operandType, resultType) 16 | { 17 | } 18 | 19 | private BoundBinaryOperator(SyntaxKind syntaxKind, BoundBinaryOperatorKind kind, TypeSymbol leftType, TypeSymbol rightType, TypeSymbol resultType) 20 | { 21 | SyntaxKind = syntaxKind; 22 | Kind = kind; 23 | LeftType = leftType; 24 | RightType = rightType; 25 | Type = resultType; 26 | } 27 | 28 | public SyntaxKind SyntaxKind { get; } 29 | public BoundBinaryOperatorKind Kind { get; } 30 | public TypeSymbol LeftType { get; } 31 | public TypeSymbol RightType { get; } 32 | public TypeSymbol Type { get; } 33 | 34 | private static BoundBinaryOperator[] _operators = 35 | { 36 | new BoundBinaryOperator(SyntaxKind.PlusToken, BoundBinaryOperatorKind.Addition, TypeSymbol.Int), 37 | new BoundBinaryOperator(SyntaxKind.MinusToken, BoundBinaryOperatorKind.Subtraction, TypeSymbol.Int), 38 | new BoundBinaryOperator(SyntaxKind.StarToken, BoundBinaryOperatorKind.Multiplication, TypeSymbol.Int), 39 | new BoundBinaryOperator(SyntaxKind.SlashToken, BoundBinaryOperatorKind.Division, TypeSymbol.Int), 40 | new BoundBinaryOperator(SyntaxKind.AmpersandToken, BoundBinaryOperatorKind.BitwiseAnd, TypeSymbol.Int), 41 | new BoundBinaryOperator(SyntaxKind.PipeToken, BoundBinaryOperatorKind.BitwiseOr, TypeSymbol.Int), 42 | new BoundBinaryOperator(SyntaxKind.HatToken, BoundBinaryOperatorKind.BitwiseXor, TypeSymbol.Int), 43 | new BoundBinaryOperator(SyntaxKind.EqualsEqualsToken, BoundBinaryOperatorKind.Equals, TypeSymbol.Int, TypeSymbol.Bool), 44 | new BoundBinaryOperator(SyntaxKind.BangEqualsToken, BoundBinaryOperatorKind.NotEquals, TypeSymbol.Int, TypeSymbol.Bool), 45 | new BoundBinaryOperator(SyntaxKind.LessToken, BoundBinaryOperatorKind.Less, TypeSymbol.Int, TypeSymbol.Bool), 46 | new BoundBinaryOperator(SyntaxKind.LessOrEqualsToken, BoundBinaryOperatorKind.LessOrEquals, TypeSymbol.Int, TypeSymbol.Bool), 47 | new BoundBinaryOperator(SyntaxKind.GreaterToken, BoundBinaryOperatorKind.Greater, TypeSymbol.Int, TypeSymbol.Bool), 48 | new BoundBinaryOperator(SyntaxKind.GreaterOrEqualsToken, BoundBinaryOperatorKind.GreaterOrEquals, TypeSymbol.Int, TypeSymbol.Bool), 49 | 50 | new BoundBinaryOperator(SyntaxKind.AmpersandToken, BoundBinaryOperatorKind.BitwiseAnd, TypeSymbol.Bool), 51 | new BoundBinaryOperator(SyntaxKind.AmpersandAmpersandToken, BoundBinaryOperatorKind.LogicalAnd, TypeSymbol.Bool), 52 | new BoundBinaryOperator(SyntaxKind.PipeToken, BoundBinaryOperatorKind.BitwiseOr, TypeSymbol.Bool), 53 | new BoundBinaryOperator(SyntaxKind.PipePipeToken, BoundBinaryOperatorKind.LogicalOr, TypeSymbol.Bool), 54 | new BoundBinaryOperator(SyntaxKind.HatToken, BoundBinaryOperatorKind.BitwiseXor, TypeSymbol.Bool), 55 | new BoundBinaryOperator(SyntaxKind.EqualsEqualsToken, BoundBinaryOperatorKind.Equals, TypeSymbol.Bool), 56 | new BoundBinaryOperator(SyntaxKind.BangEqualsToken, BoundBinaryOperatorKind.NotEquals, TypeSymbol.Bool), 57 | 58 | new BoundBinaryOperator(SyntaxKind.PlusToken, BoundBinaryOperatorKind.Addition, TypeSymbol.String), 59 | new BoundBinaryOperator(SyntaxKind.EqualsEqualsToken, BoundBinaryOperatorKind.Equals, TypeSymbol.String, TypeSymbol.Bool), 60 | new BoundBinaryOperator(SyntaxKind.BangEqualsToken, BoundBinaryOperatorKind.NotEquals, TypeSymbol.String, TypeSymbol.Bool), 61 | 62 | new BoundBinaryOperator(SyntaxKind.EqualsEqualsToken, BoundBinaryOperatorKind.Equals, TypeSymbol.Any), 63 | new BoundBinaryOperator(SyntaxKind.BangEqualsToken, BoundBinaryOperatorKind.NotEquals, TypeSymbol.Any) 64 | }; 65 | 66 | public static BoundBinaryOperator? Bind(SyntaxKind syntaxKind, TypeSymbol leftType, TypeSymbol rightType) 67 | { 68 | foreach (var op in _operators) 69 | { 70 | if (op.SyntaxKind == syntaxKind && op.LeftType == leftType && op.RightType == rightType) 71 | return op; 72 | } 73 | 74 | return null; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/episode-06.md: -------------------------------------------------------------------------------- 1 | # Episode 6 2 | 3 | [Video](https://www.youtube.com/watch?v=M0mEvzfObN0&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=6) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/21) | 5 | [Previous](episode-05.md) | 6 | [Next](episode-07.md) 7 | 8 | ## Completed items 9 | 10 | * Added colorization to REPL 11 | * Added compilation unit 12 | * Added chaining to compilations 13 | * Added statements 14 | * Added variable declaration statements 15 | 16 | ## Interesting aspects 17 | 18 | ### Scoping and shadowing 19 | 20 | Logically, scopes are a tree and mirror the structure of the code, for example: 21 | 22 | ``` 23 | { 24 | var x = 10 25 | { 26 | var y = x * 2 27 | { 28 | var z = x * y 29 | } 30 | { 31 | var result = x + y 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | The outermost scope contains `x`. Within that, there is a nested scope that 38 | contains `y`. Within that, there are two more scopes, one containing `z` and one 39 | containing `result`. 40 | 41 | Some programming languages, such as C, allow *shadowing* which means that a 42 | nested scope can declare variables that conflict with names from an outer scope. 43 | This means that within that scope the new name takes precedence, i.e. *shadows* 44 | the name coming from the outer scope. Other languages, such as C#, disallow 45 | that. In C#, only scopes that aren't in a parent-child relationship can have 46 | conflicting names. For instance, it would be valid to name `result` as `z` as 47 | the these two scopes are peers, but it wouldn't be valid to name `z` as `y` 48 | because it would conflict with the `y` coming from the parent scope. 49 | 50 | We're currently not very picky and allow shadowing. 51 | 52 | We use the [BoundScope] class to represent scopes during binding. Before binding 53 | nested statements, we [create a new scope][scoping]: 54 | 55 | ```C# 56 | private BoundStatement BindBlockStatement(BlockStatementSyntax syntax) 57 | { 58 | var statements = ImmutableArray.CreateBuilder(); 59 | _scope = new BoundScope(_scope); 60 | 61 | foreach (var statementSyntax in syntax.Statements) 62 | { 63 | var statement = BindStatement(statementSyntax); 64 | statements.Add(statement); 65 | } 66 | 67 | _scope = _scope.Parent; 68 | 69 | return new BoundBlockStatement(statements.ToImmutable()); 70 | } 71 | ``` 72 | 73 | [BoundScope]: https://github.com/terrajobst/minsk/blob/9ac348f761419a8f2b5839a6105d38b18b291f37/src/Minsk/CodeAnalysis/Binding/BoundScope.cs#L6 74 | [scoping]: https://github.com/terrajobst/minsk/blob/9ac348f761419a8f2b5839a6105d38b18b291f37/src/Minsk/CodeAnalysis/Binding/Binder.cs#L78-L86 75 | 76 | ### Submissions 77 | 78 | In a read-eval-print-loop (REPL) environment everything is ad hoc. Thus, it's 79 | often useful to be able to redeclare variables one has declared earlier, with a 80 | different type if necessary. So logically, you can think of the individual 81 | submissions to the REPL as nesting where the previous submission is a parent of 82 | the current submission (which means the first submission is the root). 83 | 84 | Given that we allow shadowing we can model this as representing the previous 85 | submissions as parents of the current scope. To do this, we've down a few 86 | things: 87 | 88 | 1. We allow [compilations to be chained][chaining]. In other words, subsequent 89 | submissions create a new `Compilation` by calling 90 | `previousCompilation.ContinueWith(syntaxTree)`. 91 | 92 | 2. When binding the new tree, we pass in the [previous compilation's 93 | state][pass-state]. 94 | 95 | 3. The binder then creates a [hierarchy of scopes][create-scope]. 96 | 97 | [chaining]: https://github.com/terrajobst/minsk/blob/9ac348f761419a8f2b5839a6105d38b18b291f37/src/Minsk/CodeAnalysis/Compilation.cs#L43-L46 98 | [pass-state]: https://github.com/terrajobst/minsk/blob/9ac348f761419a8f2b5839a6105d38b18b291f37/src/Minsk/CodeAnalysis/Compilation.cs#L35 99 | [create-scope]: https://github.com/terrajobst/minsk/blob/9ac348f761419a8f2b5839a6105d38b18b291f37/src/Minsk/CodeAnalysis/Binding/Binder.cs#L34-L56 100 | 101 | ## Expression statements 102 | 103 | Languages that separate expressions from statements often allow a specific set 104 | of expressions as statements, for example, assignments, and method calls. We 105 | currently allow any expression to be statements, even ones like `12 + 12`. Since 106 | we currently only experience our language through a REPL, this makes sense. 107 | 108 | However, when we're starting to process actual files we'll probably disallow 109 | expressions to be used in statements if they have no side effects as they don't 110 | do anything but heat up the CPU. But at that point we probably also want to 111 | add an option to the compilation that indicates whether or not the compilation 112 | is in REPL mode, in which case we'd allow them again. -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/SyntaxTree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Threading; 6 | using Minsk.CodeAnalysis.Text; 7 | 8 | namespace Minsk.CodeAnalysis.Syntax 9 | { 10 | public sealed class SyntaxTree 11 | { 12 | private Dictionary? _parents; 13 | 14 | private delegate void ParseHandler(SyntaxTree syntaxTree, 15 | out CompilationUnitSyntax root, 16 | out ImmutableArray diagnostics); 17 | 18 | private SyntaxTree(SourceText text, ParseHandler handler) 19 | { 20 | Text = text; 21 | 22 | handler(this, out var root, out var diagnostics); 23 | 24 | Diagnostics = diagnostics; 25 | Root = root; 26 | } 27 | 28 | public SourceText Text { get; } 29 | public ImmutableArray Diagnostics { get; } 30 | public CompilationUnitSyntax Root { get; } 31 | 32 | public static SyntaxTree Load(string fileName) 33 | { 34 | var text = File.ReadAllText(fileName); 35 | var sourceText = SourceText.From(text, fileName); 36 | return Parse(sourceText); 37 | } 38 | 39 | private static void Parse(SyntaxTree syntaxTree, out CompilationUnitSyntax root, out ImmutableArray diagnostics) 40 | { 41 | var parser = new Parser(syntaxTree); 42 | root = parser.ParseCompilationUnit(); 43 | diagnostics = parser.Diagnostics.ToImmutableArray(); 44 | } 45 | 46 | public static SyntaxTree Parse(string text) 47 | { 48 | var sourceText = SourceText.From(text); 49 | return Parse(sourceText); 50 | } 51 | 52 | public static SyntaxTree Parse(SourceText text) 53 | { 54 | return new SyntaxTree(text, Parse); 55 | } 56 | 57 | public static ImmutableArray ParseTokens(string text, bool includeEndOfFile = false) 58 | { 59 | var sourceText = SourceText.From(text); 60 | return ParseTokens(sourceText, includeEndOfFile); 61 | } 62 | 63 | public static ImmutableArray ParseTokens(string text, out ImmutableArray diagnostics, bool includeEndOfFile = false) 64 | { 65 | var sourceText = SourceText.From(text); 66 | return ParseTokens(sourceText, out diagnostics, includeEndOfFile); 67 | } 68 | 69 | public static ImmutableArray ParseTokens(SourceText text, bool includeEndOfFile = false) 70 | { 71 | return ParseTokens(text, out _, includeEndOfFile); 72 | } 73 | 74 | public static ImmutableArray ParseTokens(SourceText text, out ImmutableArray diagnostics, bool includeEndOfFile = false) 75 | { 76 | var tokens = new List(); 77 | 78 | void ParseTokens(SyntaxTree st, out CompilationUnitSyntax root, out ImmutableArray d) 79 | { 80 | var l = new Lexer(st); 81 | while (true) 82 | { 83 | var token = l.Lex(); 84 | 85 | if (token.Kind != SyntaxKind.EndOfFileToken || includeEndOfFile) 86 | tokens.Add(token); 87 | 88 | if (token.Kind == SyntaxKind.EndOfFileToken) 89 | { 90 | root = new CompilationUnitSyntax(st, ImmutableArray.Empty, token); 91 | break; 92 | } 93 | } 94 | 95 | d = l.Diagnostics.ToImmutableArray(); 96 | } 97 | 98 | var syntaxTree = new SyntaxTree(text, ParseTokens); 99 | diagnostics = syntaxTree.Diagnostics.ToImmutableArray(); 100 | return tokens.ToImmutableArray(); 101 | } 102 | 103 | internal SyntaxNode? GetParent(SyntaxNode syntaxNode) 104 | { 105 | if (_parents == null) 106 | { 107 | var parents = CreateParentsDictionary(Root); 108 | Interlocked.CompareExchange(ref _parents, parents, null); 109 | } 110 | 111 | return _parents[syntaxNode]; 112 | } 113 | 114 | private Dictionary CreateParentsDictionary(CompilationUnitSyntax root) 115 | { 116 | var result = new Dictionary(); 117 | result.Add(root, null); 118 | CreateParentsDictionary(result, root); 119 | return result; 120 | } 121 | 122 | private void CreateParentsDictionary(Dictionary result, SyntaxNode node) 123 | { 124 | foreach (var child in node.GetChildren()) 125 | { 126 | result.Add(child, node); 127 | CreateParentsDictionary(result, child); 128 | } 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/ConstantFolding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minsk.CodeAnalysis.Symbols; 3 | 4 | namespace Minsk.CodeAnalysis.Binding 5 | { 6 | internal static class ConstantFolding 7 | { 8 | public static BoundConstant? Fold(BoundUnaryOperator op, BoundExpression operand) 9 | { 10 | if (operand.ConstantValue != null) 11 | { 12 | switch (op.Kind) 13 | { 14 | case BoundUnaryOperatorKind.Identity: 15 | return new BoundConstant((int)operand.ConstantValue.Value); 16 | case BoundUnaryOperatorKind.Negation: 17 | return new BoundConstant(-(int)operand.ConstantValue.Value); 18 | case BoundUnaryOperatorKind.LogicalNegation: 19 | return new BoundConstant(!(bool)operand.ConstantValue.Value); 20 | case BoundUnaryOperatorKind.OnesComplement: 21 | return new BoundConstant(~(int)operand.ConstantValue.Value); 22 | default: 23 | throw new Exception($"Unexpected unary operator {op.Kind}"); 24 | } 25 | } 26 | 27 | return null; 28 | } 29 | 30 | public static BoundConstant? Fold(BoundExpression left, BoundBinaryOperator op, BoundExpression right) 31 | { 32 | var leftConstant = left.ConstantValue; 33 | var rightConstant = right.ConstantValue; 34 | 35 | // Special case && and || because there are cases where only one 36 | // side needs to be known. 37 | 38 | if (op.Kind == BoundBinaryOperatorKind.LogicalAnd) 39 | { 40 | if (leftConstant != null && !(bool)leftConstant.Value || 41 | rightConstant != null && !(bool)rightConstant.Value) 42 | { 43 | return new BoundConstant(false); 44 | } 45 | } 46 | 47 | if (op.Kind == BoundBinaryOperatorKind.LogicalOr) 48 | { 49 | if (leftConstant != null && (bool)leftConstant.Value || 50 | rightConstant != null && (bool)rightConstant.Value) 51 | { 52 | return new BoundConstant(true); 53 | } 54 | } 55 | 56 | if (leftConstant == null || rightConstant == null) 57 | return null; 58 | 59 | var l = leftConstant.Value; 60 | var r = rightConstant.Value; 61 | 62 | switch (op.Kind) 63 | { 64 | case BoundBinaryOperatorKind.Addition: 65 | if (left.Type == TypeSymbol.Int) 66 | return new BoundConstant((int)l + (int)r); 67 | else 68 | return new BoundConstant((string)l + (string)r); 69 | case BoundBinaryOperatorKind.Subtraction: 70 | return new BoundConstant((int)l - (int)r); 71 | case BoundBinaryOperatorKind.Multiplication: 72 | return new BoundConstant((int)l * (int)r); 73 | case BoundBinaryOperatorKind.Division: 74 | return new BoundConstant((int)l / (int)r); 75 | case BoundBinaryOperatorKind.BitwiseAnd: 76 | if (left.Type == TypeSymbol.Int) 77 | return new BoundConstant((int)l & (int)r); 78 | else 79 | return new BoundConstant((bool)l & (bool)r); 80 | case BoundBinaryOperatorKind.BitwiseOr: 81 | if (left.Type == TypeSymbol.Int) 82 | return new BoundConstant((int)l | (int)r); 83 | else 84 | return new BoundConstant((bool)l | (bool)r); 85 | case BoundBinaryOperatorKind.BitwiseXor: 86 | if (left.Type == TypeSymbol.Int) 87 | return new BoundConstant((int)l ^ (int)r); 88 | else 89 | return new BoundConstant((bool)l ^ (bool)r); 90 | case BoundBinaryOperatorKind.LogicalAnd: 91 | return new BoundConstant((bool)l && (bool)r); 92 | case BoundBinaryOperatorKind.LogicalOr: 93 | return new BoundConstant((bool)l || (bool)r); 94 | case BoundBinaryOperatorKind.Equals: 95 | return new BoundConstant(Equals(l, r)); 96 | case BoundBinaryOperatorKind.NotEquals: 97 | return new BoundConstant(!Equals(l, r)); 98 | case BoundBinaryOperatorKind.Less: 99 | return new BoundConstant((int)l < (int)r); 100 | case BoundBinaryOperatorKind.LessOrEquals: 101 | return new BoundConstant((int)l <= (int)r); 102 | case BoundBinaryOperatorKind.Greater: 103 | return new BoundConstant((int)l > (int)r); 104 | case BoundBinaryOperatorKind.GreaterOrEquals: 105 | return new BoundConstant((int)l >= (int)r); 106 | default: 107 | throw new Exception($"Unexpected binary operator {op.Kind}"); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Syntax/SyntaxNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Minsk.CodeAnalysis.Text; 6 | 7 | namespace Minsk.CodeAnalysis.Syntax 8 | { 9 | public abstract class SyntaxNode 10 | { 11 | private protected SyntaxNode(SyntaxTree syntaxTree) 12 | { 13 | SyntaxTree = syntaxTree; 14 | } 15 | 16 | public SyntaxTree SyntaxTree { get; } 17 | 18 | public SyntaxNode? Parent => SyntaxTree.GetParent(this); 19 | 20 | public abstract SyntaxKind Kind { get; } 21 | 22 | public virtual TextSpan Span 23 | { 24 | get 25 | { 26 | var first = GetChildren().First().Span; 27 | var last = GetChildren().Last().Span; 28 | return TextSpan.FromBounds(first.Start, last.End); 29 | } 30 | } 31 | 32 | public virtual TextSpan FullSpan 33 | { 34 | get 35 | { 36 | var first = GetChildren().First().FullSpan; 37 | var last = GetChildren().Last().FullSpan; 38 | return TextSpan.FromBounds(first.Start, last.End); 39 | } 40 | } 41 | 42 | public TextLocation Location => new TextLocation(SyntaxTree.Text, Span); 43 | 44 | public IEnumerable AncestorsAndSelf() 45 | { 46 | var node = this; 47 | while (node != null) 48 | { 49 | yield return node; 50 | node = node.Parent; 51 | } 52 | } 53 | 54 | public IEnumerable Ancestors() 55 | { 56 | return AncestorsAndSelf().Skip(1); 57 | } 58 | 59 | public abstract IEnumerable GetChildren(); 60 | 61 | public SyntaxToken GetLastToken() 62 | { 63 | if (this is SyntaxToken token) 64 | return token; 65 | 66 | // A syntax node should always contain at least 1 token. 67 | return GetChildren().Last().GetLastToken(); 68 | } 69 | 70 | public void WriteTo(TextWriter writer) 71 | { 72 | PrettyPrint(writer, this); 73 | } 74 | 75 | private static void PrettyPrint(TextWriter writer, SyntaxNode node, string indent = "", bool isLast = true) 76 | { 77 | var isToConsole = writer == Console.Out; 78 | var token = node as SyntaxToken; 79 | 80 | if (token != null) 81 | { 82 | foreach (var trivia in token.LeadingTrivia) 83 | { 84 | if (isToConsole) 85 | Console.ForegroundColor = ConsoleColor.DarkGray; 86 | 87 | writer.Write(indent); 88 | writer.Write("├──"); 89 | 90 | if (isToConsole) 91 | Console.ForegroundColor = ConsoleColor.DarkGreen; 92 | 93 | writer.WriteLine($"L: {trivia.Kind}"); 94 | } 95 | } 96 | 97 | var hasTrailingTrivia = token != null && token.TrailingTrivia.Any(); 98 | var tokenMarker = !hasTrailingTrivia && isLast ? "└──" : "├──"; 99 | 100 | if (isToConsole) 101 | Console.ForegroundColor = ConsoleColor.DarkGray; 102 | 103 | writer.Write(indent); 104 | writer.Write(tokenMarker); 105 | 106 | if (isToConsole) 107 | Console.ForegroundColor = node is SyntaxToken ? ConsoleColor.Blue : ConsoleColor.Cyan; 108 | 109 | writer.Write(node.Kind); 110 | 111 | if (token != null && token.Value != null) 112 | { 113 | writer.Write(" "); 114 | writer.Write(token.Value); 115 | } 116 | 117 | if (isToConsole) 118 | Console.ResetColor(); 119 | 120 | writer.WriteLine(); 121 | 122 | if (token != null) 123 | { 124 | foreach (var trivia in token.TrailingTrivia) 125 | { 126 | var isLastTrailingTrivia = trivia == token.TrailingTrivia.Last(); 127 | var triviaMarker = isLast && isLastTrailingTrivia ? "└──" : "├──"; 128 | 129 | if (isToConsole) 130 | Console.ForegroundColor = ConsoleColor.DarkGray; 131 | 132 | writer.Write(indent); 133 | writer.Write(triviaMarker); 134 | 135 | if (isToConsole) 136 | Console.ForegroundColor = ConsoleColor.DarkGreen; 137 | 138 | writer.WriteLine($"T: {trivia.Kind}"); 139 | } 140 | } 141 | 142 | indent += isLast ? " " : "│ "; 143 | 144 | var lastChild = node.GetChildren().LastOrDefault(); 145 | 146 | foreach (var child in node.GetChildren()) 147 | PrettyPrint(writer, child, indent, child == lastChild); 148 | } 149 | 150 | public override string ToString() 151 | { 152 | using (var writer = new StringWriter()) 153 | { 154 | WriteTo(writer); 155 | return writer.ToString(); 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /docs/episode-15.md: -------------------------------------------------------------------------------- 1 | # Episode 15 2 | 3 | [Video](https://www.youtube.com/watch?v=F0ZeU0aSkfQ&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=16) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/80) | 5 | [Previous](episode-14.md) | 6 | [Next](episode-16.md) 7 | 8 | ## Completed items 9 | 10 | * Added support for multiple syntax trees in a single compilation 11 | * Added a compiler project `mc` that accepts the program as paths & runs it 12 | * Added support for running the compiler from inside VS Code, with diagnostics 13 | showing up in the Problems pane 14 | 15 | ## Interesting aspects 16 | 17 | ### Having nodes know the syntax tree they are contained in 18 | 19 | In order to support multiple files, the parser and binder will need to report 20 | diagnostics that knows which source file, or `SourceText`, they are for. Right 21 | now, we only report diagnostics for a span, but we don't know which file that 22 | span is in. Since we're taking the span usually from a given token or syntax 23 | node, it's easiest if a given token or node would inherently know which 24 | `SourceText` they belong to. In fact, it would even more more useful if they 25 | would know which `SyntaxTree` they are contained in. However, given that tokens 26 | and nodes are immutable, this isn't as straight forward as it seems: each node 27 | wants its children in the constructor and the syntax tree wants its root in the 28 | constructor. This means we can neither construct the tree first nor the nodes 29 | first. However, we can cheat and have the `SyntaxTree` constructor run the parse 30 | method and pass itself to it: 31 | 32 | ```C# 33 | partial class SyntaxTree 34 | { 35 | private SyntaxTree(SourceText text) 36 | { 37 | Text = text; 38 | 39 | var parser = new Parser(syntaxTree); 40 | Root = parser.ParseCompilationUnit(); 41 | Diagnostics = parser.Diagnostics.ToImmutableArray(); 42 | } 43 | } 44 | ``` 45 | 46 | This way, the parser can pass the syntax tree to all nodes and the syntax tree 47 | constructor can assign the root without anyone violating the immutability 48 | guarantees. 49 | 50 | Having the nodes know the syntax tree allows us to cheat in other areas as well: 51 | eventually we'll want nodes to know their parent. This can be achieved by having 52 | the syntax tree contain a lazily computed dictionary from child to parent which 53 | it populates on first use by talking the root top-down. The syntax node would 54 | use that to return the value for its parent property. 55 | 56 | Knowing the parent node simplifies common operations in an IDE where a location 57 | needs to be used to find nodes in the tree. It's much easier to have an API that 58 | allows to find the token containing the position and then let the consumer walk 59 | upwards in the tree to find what the are looking for, such as the first 60 | containing expression, statement, or function declaration. 61 | 62 | ### Lexing individual tokens 63 | 64 | Since `SyntaxToken` is derived from `SyntaxNode` they also know the syntax tree 65 | they are contained in. This poses challenges when we need to produce standalone 66 | tokens without parsing, for example when doing syntax highlighting or in our 67 | unit tests. We need to decide what we want to happen in this case: 68 | 69 | One option is to return `null` for the syntax tree but this would make 70 | everything else a bit more complicated because now random parts of the compiler 71 | API accepting tokens would now have to check for `null`. 72 | 73 | It's easier to fabricate a fake syntax tree, that is a syntax tree whose root is 74 | a compilation with not contents. 75 | 76 | To achieve that, we generalize the `SyntaxTree` constructor by extracting the 77 | parsing into a delegate that produces the root node and the diagnostics. When 78 | lexing individual tokens, we only return lexer diagnostics and produce an empty 79 | root: 80 | 81 | ```C# 82 | partial class SyntaxTree 83 | { 84 | private delegate void ParseHandler(SyntaxTree syntaxTree, 85 | out CompilationUnitSyntax root, 86 | out ImmutableArray diagnostics); 87 | 88 | private SyntaxTree(SourceText text, ParseHandler handler) 89 | { 90 | Text = text; 91 | 92 | handler(this, out var root, out var diagnostics); 93 | 94 | Diagnostics = diagnostics; 95 | Root = root; 96 | } 97 | 98 | public static ImmutableArray ParseTokens(SourceText text, 99 | out ImmutableArray diagnostics) 100 | { 101 | var tokens = new List(); 102 | 103 | void ParseTokens(SyntaxTree st, out CompilationUnitSyntax root, out ImmutableArray d) 104 | { 105 | root = null; 106 | 107 | var lexer = new Lexer(st); 108 | while (true) 109 | { 110 | var token = lexer.Lex(); 111 | if (token.Kind == SyntaxKind.EndOfFileToken) 112 | { 113 | root = new CompilationUnitSyntax(st, ImmutableArray.Empty, token); 114 | break; 115 | } 116 | 117 | tokens.Add(token); 118 | } 119 | 120 | d = lexer.Diagnostics.ToImmutableArray(); 121 | } 122 | 123 | var syntaxTree = new SyntaxTree(text, ParseTokens); 124 | diagnostics = syntaxTree.Diagnostics.ToImmutableArray(); 125 | return tokens.ToImmutableArray(); 126 | } 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /docs/episode-11.md: -------------------------------------------------------------------------------- 1 | # Episode 11 2 | 3 | [Video](https://www.youtube.com/watch?v=98AI4XJA7Xk&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=11) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/50) | 5 | [Previous](episode-10.md) | 6 | [Next](episode-12.md) 7 | 8 | ## Completed items 9 | 10 | We added support for calling built-in functions and convert between types. 11 | 12 | ## Interesting aspects 13 | 14 | ### Separated syntax lists 15 | 16 | When parsing call expressions, we need to represent the list of arguments. One 17 | might be tempted to just use `ImmutableArray` but this begs 18 | the question where the comma between the arguments would go in the resulting 19 | syntax tree. One could say that they aren't recorded but for IDE-like 20 | experiences we generally want to make sure that the syntax tree can be 21 | serialized back to a text document at full fidelity. This enables refactoring by 22 | modifying the syntax tree but it also makes navigating the tree easier if we can 23 | assume that locations can always be mapped to a token and thus a containing 24 | node. 25 | 26 | We could decide to introduce a new node like `ArgumentSyntax` that allows us to 27 | store the comma as an optional token. However, this also seems odd because 28 | trailing commas would be illegal as well as missing intermediary commas. Also, 29 | it easily breaks if we later support, say, reordering of arguments because we'd 30 | also have to move the comma between nodes. In short, this structure simply 31 | violates the mental model we have. 32 | 33 | So instead of doing any of that, we're introducing a special kind of list we 34 | call [`SeparatedSyntaxList`][SeparatedSyntaxList] where `T` is a 35 | `SyntaxNode`. The idea is that the list is constructed from a list of nodes and 36 | tokens, so for code like 37 | 38 | ``` 39 | add(1, 2) 40 | ``` 41 | 42 | the separated syntax list would contain the expression `1`, the comma and the 43 | expression `2`. Enumerating and indexing the list will generally skip the 44 | separators (so that `Arguments[1]` would give you the second argument rather 45 | than the first comma), however, we have a method `GetSeparator(int index)` that 46 | returns the associated separator, which we define as the next token. For the 47 | last node it will return `null` because the last node doesn't have a trailing 48 | comma. 49 | 50 | [SeparatedSyntaxList]: https://github.com/terrajobst/minsk/blob/b7f24a0c2536570681923709e0516626f87d3a57/src/Minsk/CodeAnalysis/Syntax/SeparatedSyntaxList.cs 51 | 52 | ### Built-in functions 53 | 54 | We cannot declare functions yet so we added a set of [built-in functions] that 55 | we [special case in the evaluator][func-eval]. We currently support: 56 | 57 | * `print(message: string)`. Writes to the console. 58 | * `input(): string`. Reads from the console. 59 | * `rnd(max: int)`. Returns a random number between `0` and `max - 1`. 60 | 61 | [built-in functions]: https://github.com/terrajobst/minsk/blob/53d729f2bca858b510b6c8bc3ed9a200cbd62099/src/Minsk/CodeAnalysis/Symbols/BuiltinFunctions.cs#L11-L13 62 | [func-eval]: https://github.com/terrajobst/minsk/blob/53d729f2bca858b510b6c8bc3ed9a200cbd62099/src/Minsk/CodeAnalysis/Evaluator.cs#L195-L219 63 | 64 | ### Scoping 65 | 66 | When we start to compile actual source files, we'll only allow declaring 67 | functions in the global scope, i.e. you won't be able to declare functions 68 | inside of functions. 69 | 70 | However, in a REPL experience we often want to declare a function again so that 71 | we can fix bugs. The same applies to variables. We handled this by making new 72 | submissions logically nested inside the previous submission. This gives us the 73 | ability to see all previously declared functions and variables but also allows 74 | us to redefine them. If they would all be in the same scope, we'd produce errors 75 | because we generally don't want to allow developers to declare the same variable 76 | multiple times within the same scope. 77 | 78 | To handle the [built-in functions], we're adding an [outermost scope] that 79 | contains them. This also allows developers to redefine the built-in functions if 80 | they wanted to. 81 | 82 | [outermost scope]: https://github.com/terrajobst/minsk/blob/53d729f2bca858b510b6c8bc3ed9a200cbd62099/src/Minsk/CodeAnalysis/Binding/Binder.cs#L59-L67 83 | 84 | ### Conversions 85 | 86 | One simple program we'd like to write is this: 87 | 88 | ``` 89 | for i = 1 to 10 90 | { 91 | let v = rnd(100) 92 | print(v) 93 | } 94 | ``` 95 | 96 | However, the `print` functions takes a `string`, not an `int`. We need some kind 97 | of conversion mechanism. We're using a similar syntax to Pascal where casting 98 | looks like function calls: 99 | 100 | ``` 101 | for i = 1 to 10 102 | { 103 | let v = rnd(100) 104 | print(string(v)) 105 | } 106 | ``` 107 | 108 | To bind them, we [check][bind-conversion] wether the call has exactly one 109 | argument and the name is resolving to a type. If so, we bind it as a conversion 110 | expression, otherwise as a regular function invocation. 111 | 112 | In order to decide which conversions are legal, we introduce the [`Conversion`] 113 | class. It tells us whether a given conversion from `fromType` to `toType` is 114 | valid and what kind it is. Right now, we don't support implicit conversions, but 115 | we'll add those later. 116 | 117 | [bind-conversion]: https://github.com/terrajobst/minsk/blob/eccaa40146d4330d9e8e2c48668135249edf9605/src/Minsk/CodeAnalysis/Binding/Binder.cs#L293-L294 118 | [`Conversion`]: https://github.com/terrajobst/minsk/blob/eccaa40146d4330d9e8e2c48668135249edf9605/src/Minsk/CodeAnalysis/Binding/Conversion.cs -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Binding/BoundNodeFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Diagnostics; 4 | using Minsk.CodeAnalysis.Symbols; 5 | using Minsk.CodeAnalysis.Syntax; 6 | 7 | namespace Minsk.CodeAnalysis.Binding 8 | { 9 | internal static class BoundNodeFactory 10 | { 11 | public static BoundBlockStatement Block(SyntaxNode syntax, params BoundStatement[] statements) 12 | { 13 | return new BoundBlockStatement(syntax, ImmutableArray.Create(statements)); 14 | } 15 | 16 | public static BoundVariableDeclaration VariableDeclaration(SyntaxNode syntax, VariableSymbol symbol, BoundExpression initializer) 17 | { 18 | return new BoundVariableDeclaration(syntax, symbol, initializer); 19 | } 20 | 21 | public static BoundVariableDeclaration VariableDeclaration(SyntaxNode syntax, string name, BoundExpression initializer) 22 | => VariableDeclarationInternal(syntax, name, initializer, isReadOnly: false); 23 | 24 | public static BoundVariableDeclaration ConstantDeclaration(SyntaxNode syntax, string name, BoundExpression initializer) 25 | => VariableDeclarationInternal(syntax, name, initializer, isReadOnly: true); 26 | 27 | private static BoundVariableDeclaration VariableDeclarationInternal(SyntaxNode syntax, string name, BoundExpression initializer, bool isReadOnly) 28 | { 29 | var local = new LocalVariableSymbol(name, isReadOnly, initializer.Type, initializer.ConstantValue); 30 | return new BoundVariableDeclaration(syntax, local, initializer); 31 | } 32 | 33 | public static BoundWhileStatement While(SyntaxNode syntax, BoundExpression condition, BoundStatement body, BoundLabel breakLabel, BoundLabel continueLabel) 34 | { 35 | return new BoundWhileStatement(syntax, condition, body, breakLabel, continueLabel); 36 | } 37 | 38 | public static BoundGotoStatement Goto(SyntaxNode syntax, BoundLabel label) 39 | { 40 | return new BoundGotoStatement(syntax, label); 41 | } 42 | 43 | public static BoundConditionalGotoStatement GotoTrue(SyntaxNode syntax, BoundLabel label, BoundExpression condition) 44 | => new BoundConditionalGotoStatement(syntax, label, condition, jumpIfTrue: true); 45 | 46 | public static BoundConditionalGotoStatement GotoFalse(SyntaxNode syntax, BoundLabel label, BoundExpression condition) 47 | => new BoundConditionalGotoStatement(syntax, label, condition, jumpIfTrue: false); 48 | 49 | public static BoundLabelStatement Label(SyntaxNode syntax, BoundLabel label) 50 | { 51 | return new BoundLabelStatement(syntax, label); 52 | } 53 | 54 | public static BoundNopStatement Nop(SyntaxNode syntax) 55 | { 56 | return new BoundNopStatement(syntax); 57 | } 58 | 59 | public static BoundAssignmentExpression Assignment(SyntaxNode syntax, VariableSymbol variable, BoundExpression expression) 60 | { 61 | return new BoundAssignmentExpression(syntax, variable, expression); 62 | } 63 | 64 | public static BoundBinaryExpression Binary(SyntaxNode syntax, BoundExpression left, SyntaxKind kind, BoundExpression right) 65 | { 66 | var op = BoundBinaryOperator.Bind(kind, left.Type, right.Type)!; 67 | return Binary(syntax, left, op, right); 68 | } 69 | 70 | public static BoundBinaryExpression Binary(SyntaxNode syntax, BoundExpression left, BoundBinaryOperator op, BoundExpression right) 71 | { 72 | return new BoundBinaryExpression(syntax, left, op, right); 73 | } 74 | 75 | public static BoundBinaryExpression Add(SyntaxNode syntax, BoundExpression left, BoundExpression right) 76 | => Binary(syntax, left, SyntaxKind.PlusToken, right); 77 | 78 | public static BoundBinaryExpression LessOrEqual(SyntaxNode syntax, BoundExpression left, BoundExpression right) 79 | => Binary(syntax, left, SyntaxKind.LessOrEqualsToken, right); 80 | 81 | public static BoundExpressionStatement Increment(SyntaxNode syntax, BoundVariableExpression variable) 82 | { 83 | var increment = Add(syntax, variable, Literal(syntax, 1)); 84 | var incrementAssign = new BoundAssignmentExpression(syntax, variable.Variable, increment); 85 | return new BoundExpressionStatement(syntax, incrementAssign); 86 | } 87 | 88 | public static BoundUnaryExpression Not(SyntaxNode syntax, BoundExpression condition) 89 | { 90 | Debug.Assert(condition.Type == TypeSymbol.Bool); 91 | 92 | var op = BoundUnaryOperator.Bind(SyntaxKind.BangToken, TypeSymbol.Bool); 93 | Debug.Assert(op != null); 94 | return new BoundUnaryExpression(syntax, op, condition); 95 | } 96 | 97 | public static BoundVariableExpression Variable(SyntaxNode syntax, BoundVariableDeclaration variable) 98 | { 99 | return Variable(syntax, variable.Variable); 100 | } 101 | 102 | public static BoundVariableExpression Variable(SyntaxNode syntax, VariableSymbol variable) 103 | { 104 | return new BoundVariableExpression(syntax, variable); 105 | } 106 | 107 | public static BoundLiteralExpression Literal(SyntaxNode syntax, object literal) 108 | { 109 | Debug.Assert(literal is string || literal is bool || literal is int); 110 | 111 | return new BoundLiteralExpression(syntax, literal); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /docs/episode-27.md: -------------------------------------------------------------------------------- 1 | # Episode 27 2 | 3 | [Video](https://www.youtube.com/watch?v=pfEJJ9SAppE&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=27) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/157) | 5 | [Previous](episode-26.md) | 6 | [Next](episode-28.md) 7 | 8 | ## Completed items 9 | 10 | * Add `SyntaxNode.Parent` 11 | * Add `SyntaxNode.Ancestors()` and `AncestorsAndSelf()` 12 | * Add `BoundNode.Syntax` 13 | 14 | ## Interesting aspects 15 | 16 | ### Adding a parent property to an immutable tree 17 | 18 | For an IDE it's super useful to be able to walk the syntax nodes from the leaves 19 | upwards. This allows us to build a single API that, given a position, can return 20 | the token it is contained in. Calling code can then walk the parents until it 21 | finds the node it's interesting in (for example, the containing expression, 22 | statement, or function declaration). 23 | 24 | However, in order to construct an immutable tree, one has to construct the 25 | children before the parent, which is exactly what our parser does. This raises 26 | the question how one can possibly have a `Parent` property on `SyntaxNode`. 27 | 28 | The answer is: we can cheat. 29 | 30 | The parser is executed from the constructor of `SyntaxTree`. This allows the 31 | parser to pass in the `SyntaxTree` to each node. Since the `SyntaxTree` provides 32 | access to the syntax root, we can add a method on `SyntaxTree` that constructs a 33 | dictionary from child-to-parent, which we'll produce upon first request, also 34 | known as [lazy initialization]. 35 | 36 | Lazy initialization is a common technique for immutable data structures but one 37 | thing to watch out for are race conditions. One of the reasons why we make 38 | things immutable is so that we can freely pass them around to background threads 39 | and not worry about multi-threading bugs because the data structures can't be 40 | modified. 41 | 42 | Strictly speaking, lazy initialization violates this because we have a side 43 | effect that writes to an underlying field. The trick is making sure this side 44 | effect is unobservable. We achieve this by using `Interlocked.CompareExchange` 45 | from [SyntaxNode.GetParent][syntaxtree-getparent]: 46 | 47 | ```C# 48 | internal SyntaxNode? GetParent(SyntaxNode syntaxNode) 49 | { 50 | if (_parents == null) 51 | { 52 | var parents = CreateParentsDictionary(Root); 53 | Interlocked.CompareExchange(ref _parents, parents, null); 54 | } 55 | 56 | return _parents[syntaxNode]; 57 | } 58 | ``` 59 | 60 | Logically, this code 61 | 62 | ```C# 63 | Interlocked.CompareExchange(ref _parents, parents, null); 64 | ``` 65 | 66 | is equivalent to this code: 67 | 68 | ```C# 69 | if (_parents == null) 70 | _parents = parents; 71 | ``` 72 | 73 | but it does so in an atomic fashion, that is to say, between the check and the 74 | assignment no other thread will have the opportunity to write to the underlying 75 | field. 76 | 77 | The net effect is this: some thread will be the first to assign to this field 78 | and all other threads will see this value. However, please note that multiple 79 | threads might have have called `CreateParentsDictionary` but only one thread 80 | will succeed in storing the result in the `_parent` field. So this only works if 81 | multiple threads can call `CreateParentsDictionary` without problems, which 82 | generally means it shouldn't have any observable side effects either. 83 | 84 | In the literature, `CompareExchange` is often referred to as [compare-and-swap]. 85 | 86 | You might wonder why it matters that only one thread succeeds here. In the end, 87 | it doesn't matter if we were to overwrite the previous dictionary because it 88 | contains the same information. That is true here. But sometimes you construct 89 | objects that are publicly visible. Changing already observed instances to new 90 | ones (even if they are logically equivalent) can break observers because object 91 | identity changes. So don't do that. 92 | 93 | [lazy initialization]: https://en.wikipedia.org/wiki/Lazy_initialization 94 | [syntaxtree-getparent]: https://github.com/terrajobst/minsk/blob/73462c1a8b4e876bd326340350015141cec2673e/src/Minsk/CodeAnalysis/Syntax/SyntaxTree.cs#L103-L112 95 | [compare-and-swap]: https://en.wikipedia.org/wiki/Compare-and-swap 96 | 97 | ### Map bound nodes to syntax nodes 98 | 99 | The bound tree is produced by the binder by walking the syntax tree. Right now, 100 | the bound node doesn't record the syntax node it was produced from, but having 101 | this information would be super useful: 102 | 103 | 1. **It allows mapping syntax nodes to bound nodes**. In order to provide code 104 | completion, we need to know what type a given `ExpressionSyntax` was bound 105 | to. If we can produce a mapping from `SyntaxNode` to `BoundNode` we can just 106 | look up the `BoundExpressionNode` and return its `Type` property. 107 | 108 | 2. **Allows debugging**. In order to make debugging possible, we need to 109 | associate the produced code (IL or machine code) to specific locations in the 110 | source file. This information is often called "debugging symbols". This 111 | requires the emitter to be able to get this information from a bound node. 112 | For this, it would be quite convenient if `BoundNode` had a `Syntax` 113 | property. 114 | 115 | 3. **Error reporting**. Some errors are only being detected during lowering or 116 | even emit. For example, let's say a specific language construct requires a 117 | particular API in the .NET platform in order to work. It might be 118 | inconvenient to detect this during lowering rather than during binding. 119 | However, during lowering and emit we operate on `BoundNodes`, so once again 120 | it would be quite convenient to have `BoundNode.Syntax`. 121 | 122 | This isn't complicated, but it's busy work as we now require `SyntaxNode` 123 | parameter to be added to all `BoundNode` types. 124 | -------------------------------------------------------------------------------- /src/Minsk/CodeAnalysis/Compilation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using Minsk.CodeAnalysis.Binding; 8 | using Minsk.CodeAnalysis.Emit; 9 | using Minsk.CodeAnalysis.Symbols; 10 | using Minsk.CodeAnalysis.Syntax; 11 | 12 | namespace Minsk.CodeAnalysis 13 | { 14 | public sealed class Compilation 15 | { 16 | private BoundGlobalScope? _globalScope; 17 | 18 | private Compilation(bool isScript, Compilation? previous, params SyntaxTree[] syntaxTrees) 19 | { 20 | IsScript = isScript; 21 | Previous = previous; 22 | SyntaxTrees = syntaxTrees.ToImmutableArray(); 23 | } 24 | 25 | public static Compilation Create(params SyntaxTree[] syntaxTrees) 26 | { 27 | return new Compilation(isScript: false, previous: null, syntaxTrees); 28 | } 29 | 30 | public static Compilation CreateScript(Compilation? previous, params SyntaxTree[] syntaxTrees) 31 | { 32 | return new Compilation(isScript: true, previous, syntaxTrees); 33 | } 34 | 35 | public bool IsScript { get; } 36 | public Compilation? Previous { get; } 37 | public ImmutableArray SyntaxTrees { get; } 38 | public FunctionSymbol? MainFunction => GlobalScope.MainFunction; 39 | public ImmutableArray Functions => GlobalScope.Functions; 40 | public ImmutableArray Variables => GlobalScope.Variables; 41 | 42 | internal BoundGlobalScope GlobalScope 43 | { 44 | get 45 | { 46 | if (_globalScope == null) 47 | { 48 | var globalScope = Binder.BindGlobalScope(IsScript, Previous?.GlobalScope, SyntaxTrees); 49 | Interlocked.CompareExchange(ref _globalScope, globalScope, null); 50 | } 51 | 52 | return _globalScope; 53 | } 54 | } 55 | 56 | public IEnumerable GetSymbols() 57 | { 58 | var submission = this; 59 | var seenSymbolNames = new HashSet(); 60 | 61 | var builtinFunctions = BuiltinFunctions.GetAll().ToList(); 62 | 63 | while (submission != null) 64 | { 65 | foreach (var function in submission.Functions) 66 | if (seenSymbolNames.Add(function.Name)) 67 | yield return function; 68 | 69 | foreach (var variable in submission.Variables) 70 | if (seenSymbolNames.Add(variable.Name)) 71 | yield return variable; 72 | 73 | foreach (var builtin in builtinFunctions) 74 | if (seenSymbolNames.Add(builtin.Name)) 75 | yield return builtin; 76 | 77 | submission = submission.Previous; 78 | } 79 | } 80 | 81 | private BoundProgram GetProgram() 82 | { 83 | var previous = Previous == null ? null : Previous.GetProgram(); 84 | return Binder.BindProgram(IsScript, previous, GlobalScope); 85 | } 86 | 87 | public EvaluationResult Evaluate(Dictionary variables) 88 | { 89 | if (GlobalScope.Diagnostics.Any()) 90 | return new EvaluationResult(GlobalScope.Diagnostics, null); 91 | 92 | var program = GetProgram(); 93 | 94 | // var appPath = Environment.GetCommandLineArgs()[0]; 95 | // var appDirectory = Path.GetDirectoryName(appPath); 96 | // var cfgPath = Path.Combine(appDirectory, "cfg.dot"); 97 | // var cfgStatement = !program.Statement.Statements.Any() && program.Functions.Any() 98 | // ? program.Functions.Last().Value 99 | // : program.Statement; 100 | // var cfg = ControlFlowGraph.Create(cfgStatement); 101 | // using (var streamWriter = new StreamWriter(cfgPath)) 102 | // cfg.WriteTo(streamWriter); 103 | 104 | if (program.Diagnostics.HasErrors()) 105 | return new EvaluationResult(program.Diagnostics, null); 106 | 107 | var evaluator = new Evaluator(program, variables); 108 | var value = evaluator.Evaluate(); 109 | 110 | return new EvaluationResult(program.Diagnostics, value); 111 | } 112 | 113 | public void EmitTree(TextWriter writer) 114 | { 115 | if (GlobalScope.MainFunction != null) 116 | EmitTree(GlobalScope.MainFunction, writer); 117 | else if (GlobalScope.ScriptFunction != null) 118 | EmitTree(GlobalScope.ScriptFunction, writer); 119 | } 120 | 121 | public void EmitTree(FunctionSymbol symbol, TextWriter writer) 122 | { 123 | var program = GetProgram(); 124 | symbol.WriteTo(writer); 125 | writer.WriteLine(); 126 | if (!program.Functions.TryGetValue(symbol, out var body)) 127 | return; 128 | body.WriteTo(writer); 129 | } 130 | 131 | // TODO: References should be part of the compilation, not arguments for Emit 132 | public ImmutableArray Emit(string moduleName, string[] references, string outputPath) 133 | { 134 | var parseDiagnostics = SyntaxTrees.SelectMany(st => st.Diagnostics); 135 | 136 | var diagnostics = parseDiagnostics.Concat(GlobalScope.Diagnostics).ToImmutableArray(); 137 | if (diagnostics.HasErrors()) 138 | return diagnostics; 139 | 140 | var program = GetProgram(); 141 | return Emitter.Emit(program, moduleName, references, outputPath); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Minsk/IO/TextWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CodeDom.Compiler; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using Minsk.CodeAnalysis; 8 | using Minsk.CodeAnalysis.Syntax; 9 | using Minsk.CodeAnalysis.Text; 10 | 11 | namespace Minsk.IO 12 | { 13 | public static class TextWriterExtensions 14 | { 15 | private static bool IsConsole(this TextWriter writer) 16 | { 17 | if (writer == Console.Out) 18 | return !Console.IsOutputRedirected; 19 | 20 | if (writer == Console.Error) 21 | return !Console.IsErrorRedirected && !Console.IsOutputRedirected; // Color codes are always output to Console.Out 22 | 23 | if (writer is IndentedTextWriter iw && iw.InnerWriter.IsConsole()) 24 | return true; 25 | 26 | return false; 27 | } 28 | 29 | private static void SetForeground(this TextWriter writer, ConsoleColor color) 30 | { 31 | if (writer.IsConsole()) 32 | Console.ForegroundColor = color; 33 | } 34 | 35 | private static void ResetColor(this TextWriter writer) 36 | { 37 | if (writer.IsConsole()) 38 | Console.ResetColor(); 39 | } 40 | 41 | public static void WriteKeyword(this TextWriter writer, SyntaxKind kind) 42 | { 43 | var text = SyntaxFacts.GetText(kind); 44 | Debug.Assert(kind.IsKeyword() && text != null); 45 | 46 | writer.WriteKeyword(text); 47 | } 48 | 49 | public static void WriteKeyword(this TextWriter writer, string text) 50 | { 51 | writer.SetForeground(ConsoleColor.Blue); 52 | writer.Write(text); 53 | writer.ResetColor(); 54 | } 55 | 56 | public static void WriteIdentifier(this TextWriter writer, string text) 57 | { 58 | writer.SetForeground(ConsoleColor.DarkYellow); 59 | writer.Write(text); 60 | writer.ResetColor(); 61 | } 62 | 63 | public static void WriteNumber(this TextWriter writer, string text) 64 | { 65 | writer.SetForeground(ConsoleColor.Cyan); 66 | writer.Write(text); 67 | writer.ResetColor(); 68 | } 69 | 70 | public static void WriteString(this TextWriter writer, string text) 71 | { 72 | writer.SetForeground(ConsoleColor.Magenta); 73 | writer.Write(text); 74 | writer.ResetColor(); 75 | } 76 | 77 | public static void WriteSpace(this TextWriter writer) 78 | { 79 | writer.WritePunctuation(" "); 80 | } 81 | 82 | public static void WritePunctuation(this TextWriter writer, SyntaxKind kind) 83 | { 84 | var text = SyntaxFacts.GetText(kind); 85 | Debug.Assert(text != null); 86 | 87 | writer.WritePunctuation(text); 88 | } 89 | 90 | public static void WritePunctuation(this TextWriter writer, string text) 91 | { 92 | writer.SetForeground(ConsoleColor.DarkGray); 93 | writer.Write(text); 94 | writer.ResetColor(); 95 | } 96 | 97 | public static void WriteDiagnostics(this TextWriter writer, IEnumerable diagnostics) 98 | { 99 | foreach (var diagnostic in diagnostics.Where(d => d.Location.Text == null)) 100 | { 101 | var messageColor = diagnostic.IsWarning ? ConsoleColor.DarkYellow : ConsoleColor.DarkRed; 102 | writer.SetForeground(messageColor); 103 | writer.WriteLine(diagnostic.Message); 104 | writer.ResetColor(); 105 | } 106 | 107 | foreach (var diagnostic in diagnostics.Where(d => d.Location.Text != null) 108 | .OrderBy(d => d.Location.FileName) 109 | .ThenBy(d => d.Location.Span.Start) 110 | .ThenBy(d => d.Location.Span.Length)) 111 | { 112 | var text = diagnostic.Location.Text; 113 | var fileName = diagnostic.Location.FileName; 114 | var startLine = diagnostic.Location.StartLine + 1; 115 | var startCharacter = diagnostic.Location.StartCharacter + 1; 116 | var endLine = diagnostic.Location.EndLine + 1; 117 | var endCharacter = diagnostic.Location.EndCharacter + 1; 118 | 119 | var span = diagnostic.Location.Span; 120 | var lineIndex = text.GetLineIndex(span.Start); 121 | var line = text.Lines[lineIndex]; 122 | 123 | writer.WriteLine(); 124 | 125 | var messageColor = diagnostic.IsWarning ? ConsoleColor.DarkYellow : ConsoleColor.DarkRed; 126 | writer.SetForeground(messageColor); 127 | writer.Write($"{fileName}({startLine},{startCharacter},{endLine},{endCharacter}): "); 128 | writer.WriteLine(diagnostic); 129 | writer.ResetColor(); 130 | 131 | var prefixSpan = TextSpan.FromBounds(line.Start, span.Start); 132 | var suffixSpan = TextSpan.FromBounds(span.End, line.End); 133 | 134 | var prefix = text.ToString(prefixSpan); 135 | var error = text.ToString(span); 136 | var suffix = text.ToString(suffixSpan); 137 | 138 | writer.Write(" "); 139 | writer.Write(prefix); 140 | 141 | writer.SetForeground(ConsoleColor.DarkRed); 142 | writer.Write(error); 143 | writer.ResetColor(); 144 | 145 | writer.Write(suffix); 146 | 147 | writer.WriteLine(); 148 | } 149 | 150 | writer.WriteLine(); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /docs/episode-25.md: -------------------------------------------------------------------------------- 1 | # Episode 25 2 | 3 | [Video](https://www.youtube.com/watch?v=SlHnM3aQfW0&list=PLRAdsfhKI4OWNOSfS7EUu5GRAVmze1t2y&index=25) | 4 | [Pull Request](https://github.com/terrajobst/minsk/pull/144) | 5 | [Previous](episode-24.md) | 6 | [Next](episode-26.md) 7 | 8 | ## Completed items 9 | 10 | * Use C# 8's nullable reference types in Minsk, msc, and msi 11 | - Remaining: Minks.Generators and Minsk.Tests 12 | 13 | ## Interesting aspects 14 | 15 | ### Nullable reference types 16 | 17 | [Nullable reference types][post] is a feature that enables us to express which 18 | reference types are supposed to be `null`. It's beyond the scope of these notes 19 | to fully describe this feature, so please check out the [blog post][post] and 20 | the [documentation][docs]. 21 | 22 | This feature is off by default (because it produces additional warnings), so we 23 | need to turn it on. The best approach for moving existing code to nullable 24 | reference types is as follows: 25 | 26 | 1. Enable it in the project file via `Enable` 27 | 2. Mark all files as `#nullable disable` 28 | 3. Go file by file and remove `#nullable disable`, ideally walking from your 29 | lowest layer to your highest layer to avoid having to go back to files you 30 | have already touched. After each file, fix all warnings. 31 | 32 | The nice thing about this approach is that 33 | 34 | 1. You don't have to tackle the entire code base in one step. You can check in 35 | between files and still get a build without warnings. 36 | 2. New code files will be nullable enabled by default, thus not accruing debt. 37 | 38 | In our case I cheated and did all files in one session, thus skipping the 39 | `#nullable disable` step. That works here because the code base is somewhat 40 | small (less than 10K LOC). 41 | 42 | Generally speaking, nullable reference types is a feature that also involves 43 | taste. For practical reasons, you can't physically ban `null`. The trick is 44 | making sure that things that aren't supposed to be `null` cannot be observed to 45 | be `null`. There are cases where cooperation from your code is required. 46 | Consider this: 47 | 48 | ```C# 49 | SyntaxNode[] GetNodes() 50 | { 51 | var result = new SyntaxNode[Count]; 52 | for (var i = 0; i < result.Length; i++) 53 | result[i] = GetNode(i); 54 | return result; 55 | } 56 | ``` 57 | 58 | The first line allocates an array of `SyntaxNode`. If nullable is enabled for 59 | this method, `SyntaxNode[]` means "array of non-null syntax nodes". Well, 60 | clearly you can't create an array and fill it in one operation. However, your 61 | code can make sure that there are no null values once you hand the array to 62 | other code. 63 | 64 | The type system won't always be able to tell that things aren't null while you 65 | statically know that to be true. In those case you can use the `!` suffix 66 | operator to tell the compiler "trust me, this can't be null here". 67 | 68 | For example, consider this code: 69 | 70 | ```C# 71 | private BoundStatement BindReturnStatement(ReturnStatementSyntax syntax) 72 | { 73 | var expression = syntax.Expression == null ? null : BindExpression(syntax.Expression); 74 | 75 | if (_function == null) 76 | { 77 | if (expression != null) 78 | { 79 | // Main does not support return values. 80 | _diagnostics.ReportInvalidReturnWithValueInGlobalStatements(syntax.Expression!.Location); 81 | } 82 | } 83 | 84 | // ... 85 | } 86 | ``` 87 | 88 | When reporting the error we know that `syntax.Expression` can't be `null`, 89 | otherwise `expression` would have been `null` too. However, the compiler can't 90 | know that so we helped by adding the `!` operator. 91 | 92 | If you're wrong, you will get a `NullReferenceException` or 93 | `ArgumentNullException` at runtime. While this might sound bad at first ("wait, 94 | isn't this feature supposed to get rid of all null references?") it's not that 95 | bad in practice. I found that in my own code this feature makes it much easier 96 | to reason about `null` values and greatly reduces accidental null references, 97 | although it doesn't eliminate them entirely. 98 | 99 | [post]: https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types 100 | [docs]: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references 101 | 102 | ### Using in code generator 103 | 104 | Null annotations are persisted in metadata and are exposed by the Roslyn APIs. 105 | We currently don't utilize them but it's quite simple and will be tackled in 106 | one of the upcoming episodes ([#141]). 107 | 108 | The basic issue goes like this: `SyntaxNode.GetChildren()` shouldn't return 109 | `null` nodes. However, the generator currently doesn't know which nodes can be 110 | `null` because both properties look the same: 111 | 112 | ```C# 113 | partial class ReturnStatementSyntax : StatementSyntax 114 | { 115 | public SyntaxToken ReturnKeyword { get; } 116 | public ExpressionSyntax Expression { get; } 117 | } 118 | ``` 119 | 120 | Thus, this is what the generated code for `ReturnStatementSyntax` looks like: 121 | 122 | ```C# 123 | partial class ReturnStatementSyntax 124 | { 125 | public override IEnumerable GetChildren() 126 | { 127 | yield return ReturnKeyword; 128 | yield return Expression; 129 | } 130 | } 131 | ``` 132 | 133 | However, we know that `Expression` might be `null`. So we added a `null` 134 | annotation: 135 | 136 | ```C# 137 | partial class ReturnStatementSyntax : StatementSyntax 138 | { 139 | public SyntaxToken ReturnKeyword { get; } 140 | public ExpressionSyntax? Expression { get; } 141 | } 142 | ``` 143 | 144 | Our generator should use this annotation to emit a `null` check: 145 | 146 | ```C# 147 | partial class ReturnStatementSyntax 148 | { 149 | public override IEnumerable GetChildren() 150 | { 151 | yield return ReturnKeyword; 152 | if (Expression != null) 153 | yield return Expression; 154 | } 155 | } 156 | ``` 157 | 158 | [#141]: https://github.com/terrajobst/minsk/issues/141 --------------------------------------------------------------------------------