...
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Nub.Lang.Frontend.Diagnostics;
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing;
|
||||
@@ -7,25 +8,37 @@ public class Parser
|
||||
{
|
||||
private List<Token> _tokens = [];
|
||||
private int _index;
|
||||
private List<Diagnostic> _diagnostics = [];
|
||||
|
||||
public IReadOnlyList<Diagnostic> Diagnostics => _diagnostics;
|
||||
|
||||
public ModuleNode ParseModule(List<Token> tokens, string rootFilePath)
|
||||
{
|
||||
_index = 0;
|
||||
_tokens = tokens;
|
||||
_diagnostics = [];
|
||||
|
||||
List<DefinitionNode> definitions = [];
|
||||
List<string> imports = [];
|
||||
|
||||
while (Peek().HasValue)
|
||||
{
|
||||
if (TryExpectSymbol(Symbol.Import))
|
||||
try
|
||||
{
|
||||
var name = ExpectIdentifier();
|
||||
imports.Add(name.Value);
|
||||
if (TryExpectSymbol(Symbol.Import))
|
||||
{
|
||||
var name = ExpectIdentifier();
|
||||
imports.Add(name.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
definitions.Add(ParseDefinition());
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (ParseException ex)
|
||||
{
|
||||
definitions.Add(ParseDefinition());
|
||||
_diagnostics.Add(ex.Diagnostic);
|
||||
RecoverToNextDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +48,7 @@ public class Parser
|
||||
private DefinitionNode ParseDefinition()
|
||||
{
|
||||
var startIndex = _index;
|
||||
List<Modifier> modifiers = [];
|
||||
List<ModifierToken> modifiers = [];
|
||||
|
||||
List<string> documentationParts = [];
|
||||
while (_index < _tokens.Count && _tokens[_index] is DocumentationToken commentToken)
|
||||
@@ -56,21 +69,35 @@ public class Parser
|
||||
{
|
||||
Symbol.Func => ParseFuncDefinition(startIndex, modifiers, Optional.OfNullable(documentation)),
|
||||
Symbol.Struct => ParseStruct(startIndex, modifiers, Optional.OfNullable(documentation)),
|
||||
_ => throw new Exception("Unexpected symbol: " + keyword.Symbol)
|
||||
_ => throw new ParseException(Diagnostic
|
||||
.Error($"Expected 'func' or 'struct', but found '{keyword.Symbol}'")
|
||||
.WithHelp("Valid definition keywords are 'func' and 'struct'")
|
||||
.At(keyword)
|
||||
.Build())
|
||||
};
|
||||
}
|
||||
|
||||
private DefinitionNode ParseFuncDefinition(int startIndex, List<Modifier> modifiers, Optional<string> documentation)
|
||||
private DefinitionNode ParseFuncDefinition(int startIndex, List<ModifierToken> modifiers, Optional<string> documentation)
|
||||
{
|
||||
var name = ExpectIdentifier();
|
||||
List<FuncParameter> parameters = [];
|
||||
|
||||
ExpectSymbol(Symbol.OpenParen);
|
||||
|
||||
if (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
parameters.Add(ParseFuncParameter());
|
||||
TryExpectSymbol(Symbol.Comma);
|
||||
|
||||
if (!TryExpectSymbol(Symbol.Comma) && Peek().TryGetValue(out var token) && token is not SymbolToken { Symbol: Symbol.CloseParen })
|
||||
{
|
||||
_diagnostics.Add(Diagnostic
|
||||
.Warning("Missing comma between function parameters")
|
||||
.WithHelp("Add a ',' to separate parameters")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,28 +107,37 @@ public class Parser
|
||||
returnType = ParseType();
|
||||
}
|
||||
|
||||
if (modifiers.Remove(Modifier.Extern))
|
||||
var isExtern = modifiers.RemoveAll(x => x.Modifier == Modifier.Extern) > 0;
|
||||
if (isExtern)
|
||||
{
|
||||
if (modifiers.Count != 0)
|
||||
{
|
||||
throw new Exception($"Modifiers: {string.Join(", ", modifiers)} is not valid for an extern function");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Invalid modifier for extern function: {modifiers[0].Modifier}")
|
||||
.WithHelp($"Extern functions cannot use the '{modifiers[0].Modifier}' modifier")
|
||||
.At(modifiers[0])
|
||||
.Build());
|
||||
}
|
||||
|
||||
return new ExternFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, returnType);
|
||||
}
|
||||
|
||||
var body = ParseBlock();
|
||||
var global = modifiers.Remove(Modifier.Global);
|
||||
var isGlobal = modifiers.RemoveAll(x => x.Modifier == Modifier.Global) > 0;
|
||||
|
||||
if (modifiers.Count != 0)
|
||||
{
|
||||
throw new Exception($"Modifiers: {string.Join(", ", modifiers)} is not valid for a local function");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Invalid modifiers for function: {modifiers[0].Modifier}")
|
||||
.WithHelp($"Functions cannot use the '{modifiers[0].Modifier}' modifier")
|
||||
.At(modifiers[0])
|
||||
.Build());
|
||||
}
|
||||
|
||||
return new LocalFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, body, returnType, global);
|
||||
return new LocalFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, body, returnType, isGlobal);
|
||||
}
|
||||
|
||||
private StructDefinitionNode ParseStruct(int startIndex, List<Modifier> _, Optional<string> documentation)
|
||||
private StructDefinitionNode ParseStruct(int startIndex, List<ModifierToken> _, Optional<string> documentation)
|
||||
{
|
||||
var name = ExpectIdentifier().Value;
|
||||
|
||||
@@ -181,7 +217,11 @@ public class Parser
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new Exception($"Unexpected symbol {symbol.Symbol}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Unexpected symbol '{symbol.Symbol}' after identifier")
|
||||
.WithHelp("Expected '(', '=', or ':' after identifier")
|
||||
.At(symbol)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,12 +234,20 @@ public class Parser
|
||||
Symbol.While => ParseWhile(startIndex),
|
||||
Symbol.Break => new BreakNode(GetTokensForNode(startIndex)),
|
||||
Symbol.Continue => new ContinueNode(GetTokensForNode(startIndex)),
|
||||
_ => throw new Exception($"Unexpected symbol {symbol.Symbol}")
|
||||
_ => throw new ParseException(Diagnostic
|
||||
.Error($"Unexpected symbol '{symbol.Symbol}' at start of statement")
|
||||
.WithHelp("Expected identifier, 'return', 'if', 'while', 'break', or 'continue'")
|
||||
.At(symbol)
|
||||
.Build())
|
||||
};
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new Exception($"Unexpected token type {token.GetType().Name}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Unexpected token '{token.GetType().Name}' at start of statement")
|
||||
.WithHelp("Statements must start with an identifier or keyword")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,7 +297,9 @@ public class Parser
|
||||
var token = Peek();
|
||||
if (!token.HasValue || token.Value is not SymbolToken symbolToken || !TryGetBinaryOperator(symbolToken.Symbol, out var op) ||
|
||||
GetBinaryOperatorPrecedence(op.Value) < precedence)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Next();
|
||||
var right = ParseExpression(GetBinaryOperatorPrecedence(op.Value) + 1);
|
||||
@@ -343,7 +393,14 @@ public class Parser
|
||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
parameters.Add(ParseExpression());
|
||||
TryExpectSymbol(Symbol.Comma);
|
||||
if (!TryExpectSymbol(Symbol.Comma) && Peek().TryGetValue(out var nextToken) && nextToken is not SymbolToken { Symbol: Symbol.CloseParen })
|
||||
{
|
||||
_diagnostics.Add(Diagnostic
|
||||
.Warning("Missing comma between function arguments")
|
||||
.WithHelp("Add a ',' to separate arguments")
|
||||
.At(nextToken)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
expr = new FuncCallExpressionNode(GetTokensForNode(startIndex), new FuncCall(identifier.Value, parameters));
|
||||
@@ -415,7 +472,11 @@ public class Parser
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new Exception($"Unknown symbol: {symbolToken.Symbol}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Unexpected symbol '{symbolToken.Symbol}' in expression")
|
||||
.WithHelp("Expected literal, identifier, or '(' to start expression")
|
||||
.At(symbolToken)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,14 +484,18 @@ public class Parser
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new Exception($"Unexpected token type {token.GetType().Name}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Unexpected token '{token.GetType().Name}' in expression")
|
||||
.WithHelp("Expected literal, identifier, or parenthesized expression")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
return ParsePostfixOperators(startIndex, expr);
|
||||
}
|
||||
|
||||
private ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr)
|
||||
private ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
@@ -468,7 +533,15 @@ public class Parser
|
||||
List<StatementNode> statements = [];
|
||||
while (!TryExpectSymbol(Symbol.CloseBrace))
|
||||
{
|
||||
statements.Add(ParseStatement());
|
||||
try
|
||||
{
|
||||
statements.Add(ParseStatement());
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
_diagnostics.Add(ex.Diagnostic);
|
||||
RecoverToNextStatement();
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockNode(GetTokensForNode(startIndex), statements);
|
||||
@@ -494,19 +567,33 @@ public class Parser
|
||||
return new NubArrayType(baseType);
|
||||
}
|
||||
|
||||
throw new Exception($"Unexpected token {Peek()} when parsing type");
|
||||
if (!Peek().TryGetValue(out var token))
|
||||
{
|
||||
throw new ParseException(Diagnostic.Error("Unexpected end of file while parsing type")
|
||||
.At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last()))
|
||||
.WithHelp("Expected a type name")
|
||||
.Build());
|
||||
}
|
||||
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("Invalid type syntax")
|
||||
.WithHelp("Expected type name, '^' for pointer, or '[]' for array")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
|
||||
private Token ExpectToken()
|
||||
{
|
||||
var token = Peek();
|
||||
if (!token.HasValue)
|
||||
if (!Peek().TryGetValue(out var token))
|
||||
{
|
||||
throw new Exception("Reached end of tokens");
|
||||
throw new ParseException(Diagnostic.Error("Unexpected end of file")
|
||||
.At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last()))
|
||||
.WithHelp("Expected more tokens to complete the syntax")
|
||||
.Build());
|
||||
}
|
||||
|
||||
Next();
|
||||
return token.Value;
|
||||
return token;
|
||||
}
|
||||
|
||||
private SymbolToken ExpectSymbol()
|
||||
@@ -514,44 +601,56 @@ public class Parser
|
||||
var token = ExpectToken();
|
||||
if (token is not SymbolToken symbol)
|
||||
{
|
||||
throw new Exception($"Expected {nameof(SymbolToken)} but got {token.GetType().Name}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Expected symbol, but found {token.GetType().Name}")
|
||||
.WithHelp("This position requires a symbol like '(', ')', '{', '}', etc.")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
private void ExpectSymbol(Symbol symbol)
|
||||
private void ExpectSymbol(Symbol expectedSymbol)
|
||||
{
|
||||
var token = ExpectSymbol();
|
||||
if (token.Symbol != symbol)
|
||||
if (token.Symbol != expectedSymbol)
|
||||
{
|
||||
throw new Exception($"Expected symbol {symbol} but got {token.Symbol}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Expected '{expectedSymbol}', but found '{token.Symbol}'")
|
||||
.WithHelp($"Insert '{expectedSymbol}' here")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryExpectSymbol(Symbol symbol)
|
||||
{
|
||||
var result = Peek() is { HasValue: true, Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol;
|
||||
if (result) Next();
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryExpectModifier(out Modifier modifier)
|
||||
{
|
||||
if (Peek() is { HasValue: true, Value: ModifierToken modifierToken })
|
||||
if (Peek() is { Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol)
|
||||
{
|
||||
modifier = modifierToken.Modifier;
|
||||
Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
modifier = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryExpectModifier([NotNullWhen(true)] out ModifierToken? modifier)
|
||||
{
|
||||
if (Peek() is { Value: ModifierToken modifierToken })
|
||||
{
|
||||
modifier = modifierToken;
|
||||
Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
modifier = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryExpectIdentifier([NotNullWhen(true)] out string? identifier)
|
||||
{
|
||||
if (Peek() is { HasValue: true, Value: IdentifierToken identifierToken })
|
||||
if (Peek() is { Value: IdentifierToken identifierToken })
|
||||
{
|
||||
identifier = identifierToken.Value;
|
||||
Next();
|
||||
@@ -567,12 +666,45 @@ public class Parser
|
||||
var token = ExpectToken();
|
||||
if (token is not IdentifierToken identifier)
|
||||
{
|
||||
throw new Exception($"Expected {nameof(IdentifierToken)} but got {token.GetType().Name}");
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Expected identifier, but found {token.GetType().Name}")
|
||||
.WithHelp("Provide a valid identifier name here")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private void RecoverToNextDefinition()
|
||||
{
|
||||
while (Peek().HasValue)
|
||||
{
|
||||
var token = Peek().Value;
|
||||
if (token is SymbolToken { Symbol: Symbol.Func or Symbol.Struct })
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Next();
|
||||
}
|
||||
}
|
||||
|
||||
private void RecoverToNextStatement()
|
||||
{
|
||||
while (Peek().TryGetValue(out var token))
|
||||
{
|
||||
if (token is SymbolToken { Symbol: Symbol.CloseBrace } or IdentifierToken or SymbolToken
|
||||
{
|
||||
Symbol: Symbol.Return or Symbol.If or Symbol.While or Symbol.Break or Symbol.Continue
|
||||
})
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Next();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Token> Peek()
|
||||
{
|
||||
var peekIndex = _index;
|
||||
@@ -603,4 +735,14 @@ public class Parser
|
||||
{
|
||||
return _tokens[startIndex..Math.Min(_index, _tokens.Count - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
public class ParseException : Exception
|
||||
{
|
||||
public Diagnostic Diagnostic { get; }
|
||||
|
||||
public ParseException(Diagnostic diagnostic) : base(diagnostic.Message)
|
||||
{
|
||||
Diagnostic = diagnostic;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user