Compare commits

...

3 Commits

Author SHA1 Message Date
b31a2d01c6 ... 2026-02-08 20:26:11 +01:00
6f03e2203f diagnostic 2026-02-08 19:35:03 +01:00
e20f6cd7af diangostics 2026-02-08 19:24:06 +01:00
6 changed files with 364 additions and 77 deletions

View File

@@ -0,0 +1,154 @@
namespace Compiler;
public sealed class Diagnostic(DiagnosticSeverity severity, string message, string? help, FileInfo? file)
{
public static DiagnosticBuilder Info(string message) => new DiagnosticBuilder(DiagnosticSeverity.Info, message);
public static DiagnosticBuilder Warning(string message) => new DiagnosticBuilder(DiagnosticSeverity.Warning, message);
public static DiagnosticBuilder Error(string message) => new DiagnosticBuilder(DiagnosticSeverity.Error, message);
public readonly DiagnosticSeverity Severity = severity;
public readonly string Message = message;
public readonly string? Help = help;
public readonly FileInfo? File = file;
}
public sealed class DiagnosticBuilder(DiagnosticSeverity severity, string message)
{
private FileInfo? file;
private string? help;
public DiagnosticBuilder At(string fileName, int line, int column, int length)
{
file = new FileInfo(fileName, line, column, length);
return this;
}
public DiagnosticBuilder At(string fileName, Token? token)
{
if (token != null)
{
At(fileName, token.Line, token.Column, token.Length);
}
return this;
}
public DiagnosticBuilder At(string fileName, Node? node)
{
if (node != null && node.Tokens.Count != 0)
{
// todo(nub31): Calculate length based on last token
At(fileName, node.Tokens[0]);
}
return this;
}
public DiagnosticBuilder WithHelp(string helpMessage)
{
help = helpMessage;
return this;
}
public Diagnostic Build()
{
return new Diagnostic(severity, message, help, file);
}
}
public sealed class FileInfo(string file, int line, int column, int length)
{
public readonly string File = file;
public readonly int Line = line;
public readonly int Column = column;
public readonly int Length = length;
}
public enum DiagnosticSeverity
{
Info,
Warning,
Error,
}
public sealed class CompileException(Diagnostic diagnostic) : Exception
{
public readonly Diagnostic Diagnostic = diagnostic;
}
public static class DiagnosticFormatter
{
public static void Print(Diagnostic diagnostic, TextWriter writer)
{
var (label, color) = diagnostic.Severity switch
{
DiagnosticSeverity.Info => ("info", Ansi.Cyan),
DiagnosticSeverity.Warning => ("warning", Ansi.Yellow),
DiagnosticSeverity.Error => ("error", Ansi.Red),
_ => ("unknown", Ansi.Reset),
};
writer.Write(color);
writer.Write(label);
writer.Write(Ansi.Reset);
writer.Write(": ");
writer.WriteLine(diagnostic.Message);
if (diagnostic.File is null)
return;
var file = diagnostic.File;
var lineNumberWidth = diagnostic.File.Line.ToString().Length;
writer.WriteLine($" {new string(' ', lineNumberWidth)}{file.File}:{file.Line}:{file.Column}");
writer.WriteLine($"{new string(' ', lineNumberWidth)} | ");
var sourceLine = TryReadLine(file.File, file.Line);
if (sourceLine != null)
{
writer.Write($"{file.Line.ToString().PadLeft(lineNumberWidth)} | ");
writer.WriteLine(sourceLine);
writer.Write(new string(' ', lineNumberWidth));
writer.Write(" | ");
writer.Write(new string(' ', file.Column - 1));
writer.Write(color);
writer.Write(new string('^', Math.Max(1, file.Length)));
writer.WriteLine(Ansi.Reset);
}
writer.WriteLine($"{new string(' ', lineNumberWidth)} |");
if (!string.IsNullOrWhiteSpace(diagnostic.Help))
{
writer.WriteLine($" = help: {diagnostic.Help}");
}
}
private static string? TryReadLine(string file, int line)
{
try
{
using var reader = new StreamReader(file);
for (var i = 1; i < line; i++)
{
if (reader.ReadLine() == null)
return null;
}
return reader.ReadLine();
}
catch
{
return null;
}
}
private static class Ansi
{
public const string Reset = "\e[0m";
public const string Red = "\e[31m";
public const string Yellow = "\e[33m";
public const string Cyan = "\e[36m";
}
}

View File

@@ -185,10 +185,11 @@ public sealed class Generator(List<NodeDefinition> nodes)
return node switch return node switch
{ {
NodeExpressionBinary expression => EmitExpressionBinary(expression), NodeExpressionBinary expression => EmitExpressionBinary(expression),
NodeExpressionUnary expression => EmitExpressionUnary(expression),
NodeExpressionBoolLiteral expression => expression.Value.Value ? "true" : "false", NodeExpressionBoolLiteral expression => expression.Value.Value ? "true" : "false",
NodeExpressionIntLiteral expression => expression.Value.Value.ToString(), NodeExpressionIntLiteral expression => expression.Value.Value.ToString(),
NodeExpressionStringLiteral expression => $"(struct string){{ \"{expression.Value.Value}\", {expression.Value.Value.Length} }}", NodeExpressionStringLiteral expression => $"(struct string){{ \"{expression.Value.Value}\", {expression.Value.Value.Length} }}",
NodeExpressionStructLiteral expression => EmitStructLiteral(expression), NodeExpressionStructLiteral expression => EmitExpressionStructLiteral(expression),
NodeExpressionMemberAccess expression => EmitExpressionMemberAccess(expression), NodeExpressionMemberAccess expression => EmitExpressionMemberAccess(expression),
NodeExpressionIdent expression => expression.Value.Ident, NodeExpressionIdent expression => expression.Value.Ident,
_ => throw new ArgumentOutOfRangeException(nameof(node), node, null) _ => throw new ArgumentOutOfRangeException(nameof(node), node, null)
@@ -221,23 +222,19 @@ public sealed class Generator(List<NodeDefinition> nodes)
}; };
} }
private static string CType(NodeType node, string? varName = null) private string EmitExpressionUnary(NodeExpressionUnary expression)
{ {
return node switch var target = EmitExpression(expression.Target);
return expression.Operation switch
{ {
NodeTypeVoid => "void" + (varName != null ? $" {varName}" : ""), NodeExpressionUnary.Op.Negate => $"(-{target})",
NodeTypeBool => "bool" + (varName != null ? $" {varName}" : ""), NodeExpressionUnary.Op.Invert => $"(!{target})",
NodeTypeCustom type => $"struct {type.Name.Ident}" + (varName != null ? $" {varName}" : ""), _ => throw new ArgumentOutOfRangeException()
NodeTypeSInt type => $"int{type.Width}_t" + (varName != null ? $" {varName}" : ""),
NodeTypeUInt type => $"uint{type.Width}_t" + (varName != null ? $" {varName}" : ""),
NodeTypePointer type => CType(type.To) + (varName != null ? $" *{varName}" : "*"),
NodeTypeString => "struct string" + (varName != null ? $" {varName}" : ""),
NodeTypeFunc type => $"{CType(type.ReturnType)} (*{varName})({string.Join(", ", type.Parameters.Select(p => CType(p)))})",
_ => throw new ArgumentOutOfRangeException(nameof(node), node, null)
}; };
} }
private string EmitStructLiteral(NodeExpressionStructLiteral expression) private string EmitExpressionStructLiteral(NodeExpressionStructLiteral expression)
{ {
var initializerValues = new Dictionary<string, string>(); var initializerValues = new Dictionary<string, string>();
@@ -257,6 +254,22 @@ public sealed class Generator(List<NodeDefinition> nodes)
var target = EmitExpression(expression.Target); var target = EmitExpression(expression.Target);
return $"{target}.{expression.Name.Ident}"; return $"{target}.{expression.Name.Ident}";
} }
private static string CType(NodeType node, string? varName = null)
{
return node switch
{
NodeTypeVoid => "void" + (varName != null ? $" {varName}" : ""),
NodeTypeBool => "bool" + (varName != null ? $" {varName}" : ""),
NodeTypeCustom type => $"struct {type.Name.Ident}" + (varName != null ? $" {varName}" : ""),
NodeTypeSInt type => $"int{type.Width}_t" + (varName != null ? $" {varName}" : ""),
NodeTypeUInt type => $"uint{type.Width}_t" + (varName != null ? $" {varName}" : ""),
NodeTypePointer type => CType(type.To) + (varName != null ? $" *{varName}" : "*"),
NodeTypeString => "struct string" + (varName != null ? $" {varName}" : ""),
NodeTypeFunc type => $"{CType(type.ReturnType)} (*{varName})({string.Join(", ", type.Parameters.Select(p => CType(p)))})",
_ => throw new ArgumentOutOfRangeException(nameof(node), node, null)
};
}
} }
internal class IndentedTextWriter internal class IndentedTextWriter

View File

@@ -2,21 +2,31 @@
namespace Compiler; namespace Compiler;
public sealed class Parser(List<Token> tokens) public sealed class Parser(string fileName, List<Token> tokens)
{ {
public static List<NodeDefinition> Parse(List<Token> tokens) public static List<NodeDefinition> Parse(string fileName, List<Token> tokens, out List<Diagnostic> diagnostics)
{ {
return new Parser(tokens).Parse(); return new Parser(fileName, tokens).Parse(out diagnostics);
} }
private int index; private int index;
private List<NodeDefinition> Parse() private List<NodeDefinition> Parse(out List<Diagnostic> diagnostics)
{ {
var nodes = new List<NodeDefinition>(); var nodes = new List<NodeDefinition>();
diagnostics = [];
try
{
while (Peek() != null) while (Peek() != null)
{
nodes.Add(ParseDefinition()); nodes.Add(ParseDefinition());
}
}
catch (CompileException e)
{
diagnostics.Add(e.Diagnostic);
}
return nodes; return nodes;
} }
@@ -66,7 +76,7 @@ public sealed class Parser(List<Token> tokens)
return new NodeDefinitionStruct(TokensFrom(startIndex), name, fields); return new NodeDefinitionStruct(TokensFrom(startIndex), name, fields);
} }
throw new Exception("Not a valid definition"); throw new CompileException(Diagnostic.Error("Not a valid definition").At(fileName, Peek()).Build());
} }
private NodeStatement ParseStatement() private NodeStatement ParseStatement()
@@ -135,7 +145,7 @@ public sealed class Parser(List<Token> tokens)
return new NodeStatementAssignment(TokensFrom(startIndex), target, value); return new NodeStatementAssignment(TokensFrom(startIndex), target, value);
} }
throw new Exception("Not a valid followup for expression statement"); throw new CompileException(Diagnostic.Error("Cannot use expression in statement context unless called as a function or used in assignment").At(fileName, target).Build());
} }
private NodeExpression ParseExpression(int minPrecedence = -1) private NodeExpression ParseExpression(int minPrecedence = -1)
@@ -198,6 +208,16 @@ public sealed class Parser(List<Token> tokens)
ExpectSymbol(Symbol.CloseParen); ExpectSymbol(Symbol.CloseParen);
expr = value; expr = value;
} }
else if (TryExpectSymbol(Symbol.Minus))
{
var target = ParseExpression();
expr = new NodeExpressionUnary(TokensFrom(startIndex), target, NodeExpressionUnary.Op.Negate);
}
else if (TryExpectSymbol(Symbol.Bang))
{
var target = ParseExpression();
expr = new NodeExpressionUnary(TokensFrom(startIndex), target, NodeExpressionUnary.Op.Invert);
}
else if (TryExpectIntLiteral(out var intLiteral)) else if (TryExpectIntLiteral(out var intLiteral))
{ {
expr = new NodeExpressionIntLiteral(TokensFrom(startIndex), intLiteral); expr = new NodeExpressionIntLiteral(TokensFrom(startIndex), intLiteral);
@@ -233,7 +253,7 @@ public sealed class Parser(List<Token> tokens)
} }
else else
{ {
throw new Exception("Not a valid expression leaf"); throw new CompileException(Diagnostic.Error("Expected start of expression").At(fileName, Peek()).Build());
} }
if (TryExpectSymbol(Symbol.Period)) if (TryExpectSymbol(Symbol.Period))
@@ -302,7 +322,7 @@ public sealed class Parser(List<Token> tokens)
} }
} }
throw new Exception("Not a valid type"); throw new CompileException(Diagnostic.Error("Expected type").At(fileName, Peek()).Build());
} }
private List<Token> TokensFrom(int startIndex) private List<Token> TokensFrom(int startIndex)
@@ -329,7 +349,7 @@ public sealed class Parser(List<Token> tokens)
return; return;
} }
throw new Exception($"Expected symbol '{symbol}'"); throw new CompileException(Diagnostic.Error($"Expected '{symbol.AsString()}'").At(fileName, Peek()).Build());
} }
private bool TryExpectSymbol(Symbol symbol) private bool TryExpectSymbol(Symbol symbol)
@@ -351,7 +371,7 @@ public sealed class Parser(List<Token> tokens)
return token; return token;
} }
throw new Exception("Expected ident"); throw new CompileException(Diagnostic.Error("Expected identifier").At(fileName, Peek()).Build());
} }
private bool TryExpectIdent([NotNullWhen(true)] out TokenIdent? ident) private bool TryExpectIdent([NotNullWhen(true)] out TokenIdent? ident)
@@ -409,7 +429,7 @@ public sealed class Parser(List<Token> tokens)
private Token Consume() private Token Consume()
{ {
if (index >= tokens.Count) if (index >= tokens.Count)
throw new Exception("End of tokens"); throw new CompileException(Diagnostic.Error("Unexpected end of tokens").At(fileName, Peek()).Build());
return tokens[index++]; return tokens[index++];
} }
@@ -607,7 +627,6 @@ public sealed class NodeExpressionBinary(List<Token> tokens, NodeExpression left
public readonly Op Operation = operation; public readonly Op Operation = operation;
public readonly NodeExpression Right = right; public readonly NodeExpression Right = right;
public enum Op public enum Op
{ {
Add, Add,
@@ -635,6 +654,18 @@ public sealed class NodeExpressionBinary(List<Token> tokens, NodeExpression left
} }
} }
public sealed class NodeExpressionUnary(List<Token> tokens, NodeExpression target, NodeExpressionUnary.Op op) : NodeExpression(tokens)
{
public NodeExpression Target { get; } = target;
public Op Operation { get; } = op;
public enum Op
{
Negate,
Invert,
}
}
public abstract class NodeType(List<Token> tokens) : Node(tokens); public abstract class NodeType(List<Token> tokens) : Node(tokens);
public sealed class NodeTypeVoid(List<Token> tokens) : NodeType(tokens); public sealed class NodeTypeVoid(List<Token> tokens) : NodeType(tokens);

View File

@@ -1,42 +1,33 @@
using Compiler; using Compiler;
const string contents = """ var file = File.ReadAllText("test.nub");
struct person {
age: i32 var tokens = Tokenizer.Tokenize("test.nub", file, out var tokenizerDiagnostics);
name: string
foreach (var diagnostic in tokenizerDiagnostics)
{
DiagnosticFormatter.Print(diagnostic, Console.Error);
} }
func main(): i32 { if (tokenizerDiagnostics.Any(x => x.Severity == DiagnosticSeverity.Error))
let x: i32 = 23 {
x = 24 return 1;
if true {
x = 49
} else {
x = 3
} }
let i: i32 = 0 var nodes = Parser.Parse("test.nub", tokens, out var parserDiagnostics);
x = 1 + 2 * 34 foreach (var diagnostic in parserDiagnostics)
{
while i < 10 { DiagnosticFormatter.Print(diagnostic, Console.Error);
i = i + 1
x = i
} }
let me: person = struct person { age = 21 name = "Oliver" } if (parserDiagnostics.Any(x => x.Severity == DiagnosticSeverity.Error))
{
do_something(me.name) return 1;
return x
} }
func do_something(text: string): void {
}
""";
var tokens = Tokenizer.Tokenize(contents);
var nodes = Parser.Parse(tokens);
var output = Generator.Emit(nodes); var output = Generator.Emit(nodes);
File.WriteAllText("C:/Users/oliste/repos/nub-lang/compiler/Compiler/out.c", output); File.WriteAllText("C:/Users/oliste/repos/nub-lang/compiler/Compiler/out.c", output);
return 0;

View File

@@ -3,21 +3,23 @@ using System.Text;
namespace Compiler; namespace Compiler;
public sealed class Tokenizer(string contents) public sealed class Tokenizer(string fileName, string contents)
{ {
public static List<Token> Tokenize(string contents) public static List<Token> Tokenize(string fileName, string contents, out List<Diagnostic> diagnostics)
{ {
return new Tokenizer(contents).Tokenize(); return new Tokenizer(fileName, contents).Tokenize(out diagnostics);
} }
private int index; private int index;
private int line = 1; private int line = 1;
private int column = 1; private int column = 1;
private List<Token> Tokenize() private List<Token> Tokenize(out List<Diagnostic> diagnostics)
{ {
var tokens = new List<Token>(); var tokens = new List<Token>();
diagnostics = [];
try
{
while (true) while (true)
{ {
if (!TryPeek(out var c)) if (!TryPeek(out var c))
@@ -31,6 +33,11 @@ public sealed class Tokenizer(string contents)
tokens.Add(ParseToken()); tokens.Add(ParseToken());
} }
}
catch (CompileException e)
{
diagnostics.Add(e.Diagnostic);
}
return tokens; return tokens;
} }
@@ -347,7 +354,7 @@ public sealed class Tokenizer(string contents)
private char Consume() private char Consume()
{ {
if (index >= contents.Length) if (index >= contents.Length)
throw new Exception("End of tokens"); throw new CompileException(Diagnostic.Error("Unexpected end of file").At(fileName, line, column, 0).Build());
var c = contents[index]; var c = contents[index];
@@ -470,3 +477,61 @@ public sealed class TokenKeyword(int line, int column, int length, Keyword keywo
{ {
public readonly Keyword Keyword = keyword; public readonly Keyword Keyword = keyword;
} }
public static class TokenExtensions
{
public static string AsString(this Symbol symbol)
{
return symbol switch
{
Symbol.OpenCurly => "{",
Symbol.CloseCurly => "}",
Symbol.OpenParen => "(",
Symbol.CloseParen => ")",
Symbol.Comma => ",",
Symbol.Period => ",",
Symbol.Colon => ":",
Symbol.Caret => "^",
Symbol.Bang => "!",
Symbol.Equal => "=",
Symbol.EqualEqual => "==",
Symbol.BangEqual => "!+",
Symbol.LessThan => "<",
Symbol.LessThanLessThan => "<<",
Symbol.LessThanEqual => "<=",
Symbol.GreaterThan => ">",
Symbol.GreaterThanGreaterThan => ">>",
Symbol.GreaterThanEqual => ">=",
Symbol.Plus => "+",
Symbol.PlusEqual => "+=",
Symbol.Minus => "-",
Symbol.MinusEqual => "-=",
Symbol.Star => "*",
Symbol.StarEqual => "*=",
Symbol.ForwardSlash => "/",
Symbol.ForwardSlashEqual => "/=",
Symbol.Percent => "%",
Symbol.PercentEqual => "%=",
Symbol.Ampersand => "&",
Symbol.AmpersandAmpersand => "&&",
Symbol.Pipe => "|",
Symbol.PipePipe => "||",
_ => throw new ArgumentOutOfRangeException(nameof(symbol), symbol, null)
};
}
public static string AsString(this Keyword symbol)
{
return symbol switch
{
Keyword.Func => "func",
Keyword.Struct => "struct",
Keyword.Let => "let",
Keyword.If => "if",
Keyword.Else => "else",
Keyword.While => "while",
Keyword.Return => "return",
_ => throw new ArgumentOutOfRangeException(nameof(symbol), symbol, null)
};
}
}

View File

@@ -0,0 +1,33 @@
struct person {
age: i32
name: string
}
func main(): i32 {
let x: i32 = 23
x = 24
if !true {
x = 49
return x
} else {
x = 3
}
let i: i32 = 0
x = 1 + 2 * 34
while i < 10 {
i = i + 1
x = i
}
let me: person = struct person { age = 21 name = "Oliver" }
do_something(me.name)
return x
}
func do_something(text: string): void {
}