From 07660b6c460eb8f34a5c8ab278cd2149c7f6093c Mon Sep 17 00:00:00 2001 From: Deukhoofd Date: Thu, 15 Nov 2018 15:51:05 +0100 Subject: [PATCH] Implement defining functions --- Upsilon/BaseTypes/LuaFunction.cs | 19 ++++++ Upsilon/BaseTypes/Type.cs | 1 + Upsilon/Binder/Binder.cs | 64 +++++++++++++++++-- Upsilon/Binder/BoundKind.cs | 3 +- Upsilon/Binder/BoundScope.cs | 15 +++-- .../BoundStatements/BoundFunctionStatement.cs | 21 ++++++ Upsilon/Binder/VariableSymbol.cs | 12 ++++ Upsilon/Diagnostics.cs | 5 ++ Upsilon/Evaluator/Evaluator.cs | 12 ++++ Upsilon/Evaluator/Script.cs | 8 ++- Upsilon/Parser/Lexer.cs | 2 + Upsilon/Parser/Parser.cs | 36 ++++++++++- .../FunctionStatementSyntax.cs | 47 ++++++++++++++ Upsilon/Parser/SyntaxKeyWords.cs | 2 + Upsilon/Parser/SyntaxKind.cs | 3 + UpsilonTests/FunctionTests.cs | 30 +++++++++ Ycicle/Program.cs | 2 + 17 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 Upsilon/BaseTypes/LuaFunction.cs create mode 100644 Upsilon/Binder/BoundStatements/BoundFunctionStatement.cs create mode 100644 Upsilon/Parser/StatementSyntax/FunctionStatementSyntax.cs create mode 100644 UpsilonTests/FunctionTests.cs diff --git a/Upsilon/BaseTypes/LuaFunction.cs b/Upsilon/BaseTypes/LuaFunction.cs new file mode 100644 index 0000000..7630ea6 --- /dev/null +++ b/Upsilon/BaseTypes/LuaFunction.cs @@ -0,0 +1,19 @@ +using System.Collections.Immutable; +using Upsilon.Binder; + +namespace Upsilon.BaseTypes +{ + public class LuaFunction : LuaType + { + public LuaFunction(ImmutableArray parameters, BoundBlockStatement block) + { + Parameters = parameters; + Block = block; + } + + public override Type Type => Type.Function; + + public ImmutableArray Parameters { get; } + public BoundBlockStatement Block { get; } + } +} \ No newline at end of file diff --git a/Upsilon/BaseTypes/Type.cs b/Upsilon/BaseTypes/Type.cs index 184c332..d350132 100644 --- a/Upsilon/BaseTypes/Type.cs +++ b/Upsilon/BaseTypes/Type.cs @@ -2,6 +2,7 @@ namespace Upsilon.BaseTypes { public enum Type { + Unknown, Nil, Boolean, Number, diff --git a/Upsilon/Binder/Binder.cs b/Upsilon/Binder/Binder.cs index 3bc59d7..7e2d9aa 100644 --- a/Upsilon/Binder/Binder.cs +++ b/Upsilon/Binder/Binder.cs @@ -12,10 +12,10 @@ namespace Upsilon.Binder private readonly Diagnostics _diagnostics; private BoundScope _scope; - public Binder(BoundScope parentScope, Diagnostics diagnostics) + public Binder(Diagnostics diagnostics) { _diagnostics = diagnostics; - _scope = new BoundScope(parentScope); + _scope = new BoundScope(null); } public BoundScript BindScript(BlockStatementSyntax e) @@ -24,7 +24,7 @@ namespace Upsilon.Binder return new BoundScript(bound); } - public BoundStatement BindStatement(StatementSyntax s) + private BoundStatement BindStatement(StatementSyntax s) { switch (s.Kind) { @@ -36,12 +36,14 @@ namespace Upsilon.Binder return BindBlockStatement((BlockStatementSyntax) s); case SyntaxKind.IfStatement: return BindIfStatement((IfStatementSyntax) s); + case SyntaxKind.FunctionStatement: + return BindFunctionStatement((FunctionStatementSyntax) s); } throw new NotImplementedException(s.Kind.ToString()); } - public BoundExpression BindExpression(ExpressionSyntax e) + private BoundExpression BindExpression(ExpressionSyntax e) { switch (e.Kind) { @@ -177,19 +179,21 @@ namespace Upsilon.Binder private BoundStatement BindBlockStatement(BlockStatementSyntax e) { var arr = ImmutableArray.CreateBuilder(); - var innerBinder = new Binder(_scope, _diagnostics); foreach (var statementSyntax in e.Statements) { - var bound = innerBinder.BindStatement(statementSyntax); + var bound = BindStatement(statementSyntax); arr.Add(bound); } + return new BoundBlockStatement(arr.ToImmutable()); } private BoundStatement BindIfStatement(IfStatementSyntax e) { + _scope = new BoundScope(_scope); var condition = BindExpressionStatement(e.Condition); var block = BindBlockStatement(e.Block); + _scope = _scope.ParentScope; if (e.NextElseIfStatement != null) { var nextElseIf = BindIfStatement(e.NextElseIfStatement); @@ -202,12 +206,60 @@ namespace Upsilon.Binder } else { + _scope = new BoundScope(_scope); var elseBlock = BindBlockStatement(e.ElseStatement.Block); var elseStatement = new BoundElseStatement((BoundBlockStatement) elseBlock); + _scope = _scope.ParentScope; + return new BoundIfStatement((BoundExpressionStatement) condition, (BoundBlockStatement) block, elseStatement); } } + private BoundStatement BindFunctionStatement(FunctionStatementSyntax e) + { + var name = e.Identifier.Name; + var isLocal = e.LocalToken != null; + + var innerScope = new BoundScope(_scope); + var parameters = ImmutableArray.CreateBuilder(); + foreach (var identifierToken in e.Parameters) + { + var vari = new VariableSymbol(identifierToken.Name, Type.Unknown, true); + parameters.Add(vari); + innerScope.SetVariable(vari); + } + + if (!_scope.TryGetVariable(name, !isLocal, out var variable)) + { + variable = new FunctionVariableSymbol(name, Type.Function, isLocal, parameters.ToImmutable()); + if (isLocal) + _scope.SetVariable(variable); + else + _scope.SetGlobalVariable(variable); + } + else + { + // don't allow assigning different typed variables to a variable, unless either of them is nil, allow assigning nil to all variables + if (variable.Type != Type.Function) + { + if (variable.Type == Type.Nil ) + { + variable.Type = Type.Function; + } + else + { + _diagnostics.LogCannotConvert(Type.Function, variable.Type, e.Span); + return new BoundExpressionStatement(null); + } + } + } + + _scope = innerScope; + var block = BindBlockStatement(e.Block); + _scope = _scope.ParentScope; + return new BoundFunctionStatement(variable, parameters.ToImmutable(), (BoundBlockStatement) block); + } + } } \ No newline at end of file diff --git a/Upsilon/Binder/BoundKind.cs b/Upsilon/Binder/BoundKind.cs index 89e9d9a..f66929e 100644 --- a/Upsilon/Binder/BoundKind.cs +++ b/Upsilon/Binder/BoundKind.cs @@ -14,6 +14,7 @@ namespace Upsilon.Binder BoundExpressionStatement, BoundBlockStatement, BoundIfStatement, - BoundElseStatement + BoundElseStatement, + BoundFunctionStatement } } \ No newline at end of file diff --git a/Upsilon/Binder/BoundScope.cs b/Upsilon/Binder/BoundScope.cs index 9e2a54d..0756bfc 100644 --- a/Upsilon/Binder/BoundScope.cs +++ b/Upsilon/Binder/BoundScope.cs @@ -6,20 +6,21 @@ namespace Upsilon.Binder { public class BoundScope { - private readonly BoundScope _parentScope; + public readonly BoundScope ParentScope; private readonly Dictionary _variables; public BoundScope(BoundScope parentScope) { - _parentScope = parentScope; + ParentScope = parentScope; _variables = new Dictionary(); } public BoundScope(Dictionary variables, BoundScope parentScope) { - _parentScope = parentScope; + ParentScope = parentScope; _variables = variables.ToDictionary(x => x.Key.Name, x => x.Key); } + public void SetVariable(VariableSymbol var) { if (_variables.ContainsKey(var.Name)) @@ -30,13 +31,13 @@ namespace Upsilon.Binder public void SetGlobalVariable(VariableSymbol var) { - if (_parentScope == null) + if (ParentScope == null) { SetVariable(var); } else { - _parentScope.SetGlobalVariable(var); + ParentScope.SetGlobalVariable(var); } } @@ -47,9 +48,9 @@ namespace Upsilon.Binder { return true; } - if (_parentScope != null && allowUpperScopes) + if (ParentScope != null && allowUpperScopes) { - return _parentScope.TryGetVariable(key, true, out result); + return ParentScope.TryGetVariable(key, true, out result); } return false; } diff --git a/Upsilon/Binder/BoundStatements/BoundFunctionStatement.cs b/Upsilon/Binder/BoundStatements/BoundFunctionStatement.cs new file mode 100644 index 0000000..a6854c8 --- /dev/null +++ b/Upsilon/Binder/BoundStatements/BoundFunctionStatement.cs @@ -0,0 +1,21 @@ +using System.Collections.Immutable; + +namespace Upsilon.Binder +{ + public class BoundFunctionStatement : BoundStatement + { + public VariableSymbol Identifier { get; } + public ImmutableArray Parameters { get; } + public BoundBlockStatement Block { get; } + + public BoundFunctionStatement(VariableSymbol identifier, ImmutableArray parameters, + BoundBlockStatement block) + { + Identifier = identifier; + Parameters = parameters; + Block = block; + } + + public override BoundKind Kind => BoundKind.BoundFunctionStatement; + } +} \ No newline at end of file diff --git a/Upsilon/Binder/VariableSymbol.cs b/Upsilon/Binder/VariableSymbol.cs index 907697c..6f50403 100644 --- a/Upsilon/Binder/VariableSymbol.cs +++ b/Upsilon/Binder/VariableSymbol.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using Upsilon.BaseTypes; namespace Upsilon.Binder @@ -15,4 +16,15 @@ namespace Upsilon.Binder public bool Local { get; } public string Name { get; } } + + public class FunctionVariableSymbol : VariableSymbol + { + public ImmutableArray Parameters { get; } + + public FunctionVariableSymbol(string name, Type type, bool local, ImmutableArray parameters) + : base(name, type, local) + { + Parameters = parameters; + } + } } \ No newline at end of file diff --git a/Upsilon/Diagnostics.cs b/Upsilon/Diagnostics.cs index 3af91ae..8d75348 100644 --- a/Upsilon/Diagnostics.cs +++ b/Upsilon/Diagnostics.cs @@ -25,6 +25,11 @@ namespace Upsilon Log(DiagnosticLevel.Error, message, location); } + public void LogBadCharacter(TextSpan location, SyntaxKind expectedToken, SyntaxKind currentKind) + { + LogError($"Invalid character found. Expected: '{expectedToken}', Got: '{currentKind}'", location); + } + public void LogBadCharacter(TextSpan location, SyntaxKind expectedToken) { LogError($"Invalid character found. Expected: '{expectedToken}'", location); diff --git a/Upsilon/Evaluator/Evaluator.cs b/Upsilon/Evaluator/Evaluator.cs index 87deb75..3479958 100644 --- a/Upsilon/Evaluator/Evaluator.cs +++ b/Upsilon/Evaluator/Evaluator.cs @@ -36,6 +36,9 @@ namespace Upsilon.Evaluator case BoundKind.BoundIfStatement: EvaluateBoundIfStatement((BoundIfStatement) e); break; + case BoundKind.BoundFunctionStatement: + EvaluateBoundFunctionStatement((BoundFunctionStatement) e); + break; default: EvaluateExpressionStatement((BoundExpressionStatement) e); break; @@ -155,5 +158,14 @@ namespace Upsilon.Evaluator EvaluateBoundBlockStatement(boundBlockStatement.ElseStatement.Block); } } + + private void EvaluateBoundFunctionStatement(BoundFunctionStatement boundFunctionStatement) + { + var func = new LuaFunction(boundFunctionStatement.Parameters, boundFunctionStatement.Block); + if (boundFunctionStatement.Identifier.Local) + _scope.Set(boundFunctionStatement.Identifier, func); + else + _scope.SetGlobal(boundFunctionStatement.Identifier, func); + } } } \ No newline at end of file diff --git a/Upsilon/Evaluator/Script.cs b/Upsilon/Evaluator/Script.cs index a75c711..bfb4236 100644 --- a/Upsilon/Evaluator/Script.cs +++ b/Upsilon/Evaluator/Script.cs @@ -3,6 +3,7 @@ using Upsilon.BaseTypes; using Upsilon.Binder; using Upsilon.Parser; using Upsilon.Text; +using Upsilon.Utilities; namespace Upsilon.Evaluator { @@ -22,7 +23,7 @@ namespace Upsilon.Evaluator _parsed = Parser.Parser.Parse(scriptString, Diagnostics); if (variables == null) variables = new Dictionary(); - Binder = new Binder.Binder(new BoundScope(variables, null), Diagnostics); + Binder = new Binder.Binder(Diagnostics); Scope = new EvaluationScope(variables); Evaluator = new Evaluator( Diagnostics, Scope); } @@ -41,5 +42,10 @@ namespace Upsilon.Evaluator { return (T)Evaluator.Evaluate(Bind()); } + + public string PrettyPrintSyntaxTree() + { + return _parsed.Print(); + } } } \ No newline at end of file diff --git a/Upsilon/Parser/Lexer.cs b/Upsilon/Parser/Lexer.cs index c4e4d8e..4771520 100644 --- a/Upsilon/Parser/Lexer.cs +++ b/Upsilon/Parser/Lexer.cs @@ -82,6 +82,8 @@ namespace Upsilon.Parser return new SyntaxToken(SyntaxKind.OpenParenthesis, _position, "(", null); case ')': return new SyntaxToken(SyntaxKind.CloseParenthesis, _position, ")", null); + case ',': + return new SyntaxToken(SyntaxKind.Comma, _position, ",", null); case '=': if (Next == '=') { diff --git a/Upsilon/Parser/Parser.cs b/Upsilon/Parser/Parser.cs index 9deb1be..a48e2a2 100644 --- a/Upsilon/Parser/Parser.cs +++ b/Upsilon/Parser/Parser.cs @@ -45,7 +45,7 @@ namespace Upsilon.Parser if (Current.Kind == kind) return NextToken(); - _diagnostics.LogBadCharacter(Current.Span, kind); + _diagnostics.LogBadCharacter(Current.Span, kind, Current.Kind); return new SyntaxToken(kind, Current.Span.Start, "", null); } @@ -71,6 +71,15 @@ namespace Upsilon.Parser return ParseIfStatement(SyntaxKind.IfKeyword); } + if (Current.Kind == SyntaxKind.FunctionKeyword) + { + return ParseFunctionStatement(); + } + if (Current.Kind == SyntaxKind.LocalKeyword && Next.Kind == SyntaxKind.FunctionKeyword) + { + return ParseFunctionStatement(); + } + return ParseExpressionStatement(); } @@ -114,6 +123,31 @@ namespace Upsilon.Parser } } + private StatementSyntax ParseFunctionStatement() + { + SyntaxToken localToken = null; + if (Current.Kind == SyntaxKind.LocalKeyword) + { + localToken = NextToken(); + } + var functionToken = MatchToken(SyntaxKind.FunctionKeyword); + var identifier = (IdentifierToken)MatchToken(SyntaxKind.Identifier); + var openParenthesis = MatchToken(SyntaxKind.OpenParenthesis); + var variableBuilder = ImmutableArray.CreateBuilder(); + while (Current.Kind != SyntaxKind.CloseParenthesis) + { + var variableIdentifier = (IdentifierToken)MatchToken(SyntaxKind.Identifier); + variableBuilder.Add(variableIdentifier); + if (Current.Kind == SyntaxKind.Comma) + NextToken(); + } + var closeParenthesis = MatchToken(SyntaxKind.CloseParenthesis); + var block = ParseBlockStatement(new[] {SyntaxKind.EndKeyword}); + var endToken = MatchToken(SyntaxKind.EndKeyword); + return new FunctionStatementSyntax(localToken, functionToken, identifier, openParenthesis, + variableBuilder.ToImmutable(), closeParenthesis, (BlockStatementSyntax) block, endToken); + } + private ExpressionStatementSyntax ParseExpressionStatement() { var expression = ParseExpression(); diff --git a/Upsilon/Parser/StatementSyntax/FunctionStatementSyntax.cs b/Upsilon/Parser/StatementSyntax/FunctionStatementSyntax.cs new file mode 100644 index 0000000..1fdd965 --- /dev/null +++ b/Upsilon/Parser/StatementSyntax/FunctionStatementSyntax.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Upsilon.Parser +{ + public class FunctionStatementSyntax : StatementSyntax + { + public SyntaxToken LocalToken { get; } + public SyntaxToken FunctionToken { get; } + public IdentifierToken Identifier { get; } + public SyntaxToken OpenParenthesis { get; } + public ImmutableArray Parameters { get; } + public SyntaxToken CloseParenthesis { get; } + public BlockStatementSyntax Block { get; } + public SyntaxToken EndToken { get; } + + public FunctionStatementSyntax(SyntaxToken localToken, SyntaxToken functionToken, IdentifierToken identifier, + SyntaxToken openParenthesis, ImmutableArray parameters, SyntaxToken closeParenthesis, + BlockStatementSyntax block, SyntaxToken endToken) + { + LocalToken = localToken; + FunctionToken = functionToken; + Identifier = identifier; + OpenParenthesis = openParenthesis; + Parameters = parameters; + CloseParenthesis = closeParenthesis; + Block = block; + EndToken = endToken; + } + + public override SyntaxKind Kind => SyntaxKind.FunctionStatement; + public override IEnumerable ChildNodes() + { + yield return LocalToken; + yield return FunctionToken; + yield return Identifier; + yield return OpenParenthesis; + foreach (var identifierToken in Parameters) + { + yield return identifierToken; + } + yield return CloseParenthesis; + yield return Block; + yield return EndToken; + } + } +} \ No newline at end of file diff --git a/Upsilon/Parser/SyntaxKeyWords.cs b/Upsilon/Parser/SyntaxKeyWords.cs index 3c58532..57a84a2 100644 --- a/Upsilon/Parser/SyntaxKeyWords.cs +++ b/Upsilon/Parser/SyntaxKeyWords.cs @@ -30,6 +30,8 @@ namespace Upsilon.Parser return SyntaxKind.ElseKeyword; case "nil": return SyntaxKind.NilKeyword; + case "function": + return SyntaxKind.FunctionKeyword; default: return SyntaxKind.Identifier; } diff --git a/Upsilon/Parser/SyntaxKind.cs b/Upsilon/Parser/SyntaxKind.cs index 576c6b6..45d3f0f 100644 --- a/Upsilon/Parser/SyntaxKind.cs +++ b/Upsilon/Parser/SyntaxKind.cs @@ -19,6 +19,7 @@ namespace Upsilon.Parser EqualsEquals, Tilde, TildeEquals, + Comma, // key words TrueKeyword, @@ -33,6 +34,7 @@ namespace Upsilon.Parser ElseIfKeyword, ElseKeyword, NilKeyword, + FunctionKeyword, Identifier, @@ -54,5 +56,6 @@ namespace Upsilon.Parser IfStatement, ElseIfStatement, ElseStatement, + FunctionStatement, } } \ No newline at end of file diff --git a/UpsilonTests/FunctionTests.cs b/UpsilonTests/FunctionTests.cs new file mode 100644 index 0000000..fac4f94 --- /dev/null +++ b/UpsilonTests/FunctionTests.cs @@ -0,0 +1,30 @@ +using Upsilon.BaseTypes; +using Upsilon.BaseTypes.Number; +using Upsilon.Evaluator; +using Xunit; + +namespace UpsilonTests +{ + public class FunctionTests + { + [Fact] + public void BasicFunctionTest() + { + const string input = @" +function testFunc () + a = 100 +end +a = 50 +testFunc() +"; + var script = new Script(input); + Assert.Empty(script.Diagnostics.Messages); + script.Evaluate(); + Assert.Empty(script.Diagnostics.Messages); + Assert.True(script.Scope.TryGet("testFunc", out var func)); + Assert.IsType(func); + Assert.True(script.Scope.TryGet("a", out var a)); + Assert.Equal(100, (long)(NumberLong)a); + } + } +} \ No newline at end of file diff --git a/Ycicle/Program.cs b/Ycicle/Program.cs index 0936696..57f23c4 100644 --- a/Ycicle/Program.cs +++ b/Ycicle/Program.cs @@ -29,6 +29,8 @@ namespace Ycicle } var parsed = new Script(input, variables); + Console.WriteLine(parsed.PrettyPrintSyntaxTree()); + if (parsed.Diagnostics.Messages.Count > 0) { Console.ForegroundColor = ConsoleColor.Red;