diff --git a/Loom.CLI/Program.cs b/Loom.CLI/Program.cs index 671cb169..65a266f2 100644 --- a/Loom.CLI/Program.cs +++ b/Loom.CLI/Program.cs @@ -13,6 +13,6 @@ var result = compilationUnit.Compile(); var debugInfo = result.Files .Where(f => !f.SourceFile.IsDeclaration) - .Select(f => f.GetDebugInfo(rebuilt: false, debugDiagnostics: false)); + .Select(f => f.GetDebugInfo(rebuilt: false, debugDiagnostics: loomConfig.Debug)); Console.WriteLine(string.Join(Environment.NewLine, debugInfo)); \ No newline at end of file diff --git a/Loom.Config/LoomConfig.cs b/Loom.Config/LoomConfig.cs index 5813678b..35f4e435 100644 --- a/Loom.Config/LoomConfig.cs +++ b/Loom.Config/LoomConfig.cs @@ -9,7 +9,10 @@ public sealed class LoomConfig [TomlPropertyName("no_emit")] public bool NoEmit { get; set; } - + + [TomlPropertyName("debug")] + public bool Debug { get; set; } + [TomlPropertyName("project_type")] [TomlConverter(typeof(ProjectTypeConverter))] public ProjectType ProjectType { get; init; } diff --git a/Loom.Core/Diagnostics/Diagnostic.cs b/Loom.Core/Diagnostics/Diagnostic.cs index d70af2b1..bd0c08ed 100644 --- a/Loom.Core/Diagnostics/Diagnostic.cs +++ b/Loom.Core/Diagnostics/Diagnostic.cs @@ -56,7 +56,16 @@ public sealed record Diagnostic(LocationSpan Span, DiagnosticSeverity Severity, : $"expected operand of type '{suggestion.OperandType}', not '{operand}'"; } - public override string ToString() + public override string ToString() => + Severity == DiagnosticSeverity.Debug + ? FormatCompact() + : FormatFrame(); + + private string FormatCompact() => + $"{_severityStyle.PrimaryColor}{Colors.Bold}{_severityStyle.Label}{Colors.Reset} " + + $"{Colors.Dim}[{Span}]{Colors.Reset} {Colors.Gray}{Message}{Colors.Reset}"; + + private string FormatFrame() { var lines = new List { FormatHeader(), FormatLocation(), Gutter }; AppendSource(lines); diff --git a/Loom.Core/Resolving/Resolver.cs b/Loom.Core/Resolving/Resolver.cs index 40dcb2e8..2c863850 100644 --- a/Loom.Core/Resolving/Resolver.cs +++ b/Loom.Core/Resolving/Resolver.cs @@ -22,7 +22,10 @@ public sealed class Resolver(ParserResult parserResult, CompilationUnit compilat [MemberNotNull(nameof(_semanticModel))] public SemanticModel Resolve() { - _semanticModel = new SemanticModel(parserResult.Tree, _diagnostics, _allDeclarations, _allReferences); + _semanticModel = new SemanticModel(parserResult.Tree, _diagnostics, _allDeclarations, _allReferences) + { + EmitDebugDiagnostics = compilationUnit.Config.Debug + }; PushScope(); DeclareIntrinsicSymbols(); DeclareGlobalSymbols(); @@ -699,24 +702,28 @@ private void DeclareSymbol(Symbol symbol) ); } - if (TypeChecker.EmitDebugDiagnostics) - _diagnostics.Debug(symbol.Declaration, $"Declared symbol: {symbol}"); - if (IsDeclarationFile()) - { symbol.IsGlobal = true; - if (TypeChecker.EmitDebugDiagnostics) - _diagnostics.Debug(symbol.Declaration, $"{symbol} is global"); - } if (_context == ResolverContext.Ambient) symbol.IsAmbient = true; - if (!parserResult.Tree.File.IsIntrinsic) return; + if (parserResult.Tree.File.IsIntrinsic) + symbol.IsIntrinsic = true; + + if (_semanticModel.EmitDebugDiagnostics) + _diagnostics.Debug(symbol.Declaration, DescribeDeclaration(symbol)); + } + + private static string DescribeDeclaration(Symbol symbol) + { + var flags = new List(); + if (symbol.IsGlobal) flags.Add("global"); + if (symbol.IsAmbient) flags.Add("ambient"); + if (symbol.IsIntrinsic) flags.Add("intrinsic"); - symbol.IsIntrinsic = true; - if (TypeChecker.EmitDebugDiagnostics) - _diagnostics.Debug(symbol.Declaration, $"{symbol} is intrinsic"); + var suffix = flags.Count > 0 ? $" [{string.Join(", ", flags)}]" : ""; + return $"Declared '{symbol.Name}' ({symbol.Kind}){suffix}"; } private void AddToLookup(Symbol symbol) diff --git a/Loom.Core/Resolving/SemanticModel.cs b/Loom.Core/Resolving/SemanticModel.cs index 0bd965f8..e56456f9 100644 --- a/Loom.Core/Resolving/SemanticModel.cs +++ b/Loom.Core/Resolving/SemanticModel.cs @@ -12,6 +12,7 @@ public sealed record SemanticModel(Tree Tree, DiagnosticBag Diagnostics, SymbolT : DiagnosedResult(Diagnostics) { public bool DisableRuntimeLibraryImport { get; set; } + public bool EmitDebugDiagnostics { get; set; } public bool MustImportRuntimeLibrary => !DisableRuntimeLibraryImport && !Tree.File.IsIntrinsic diff --git a/Loom.Core/TypeChecking/TypeChecker.cs b/Loom.Core/TypeChecking/TypeChecker.cs index 27d1529c..64bc9838 100644 --- a/Loom.Core/TypeChecking/TypeChecker.cs +++ b/Loom.Core/TypeChecking/TypeChecker.cs @@ -26,8 +26,6 @@ namespace Loom.Core.TypeChecking; public sealed partial class TypeChecker : Visitor { - public static bool EmitDebugDiagnostics { get; set; } - private readonly DiagnosticBag _diagnostics = new(); private readonly Dictionary _exitStates = []; private readonly Stack> _loopExitScopes = []; @@ -1184,7 +1182,7 @@ private T BindType(Node node, T type) where T : Type { _semanticModel.TypeSolver.SetType(node, type); - if (!EmitDebugDiagnostics || node is Tree or ExpressionStatement) + if (!_semanticModel.EmitDebugDiagnostics || node is Tree or ExpressionStatement) return type; var simplified = TypeSimplifier.Simplify(type); diff --git a/Loom.Testing/ConfigReaderTest.cs b/Loom.Testing/ConfigReaderTest.cs new file mode 100644 index 00000000..0a2f1b55 --- /dev/null +++ b/Loom.Testing/ConfigReaderTest.cs @@ -0,0 +1,23 @@ +using Loom.Config; +using Tomlyn; + +namespace Loom.Testing; + +public class ConfigReaderTest +{ + [Fact] + public void Debug_DefaultsToFalse() + { + var config = TomlSerializer.Deserialize("project_type = \"game\""); + Assert.NotNull(config); + Assert.False(config.Debug); + } + + [Fact] + public void Debug_ParsesTrue() + { + var config = TomlSerializer.Deserialize("project_type = \"game\"\ndebug = true"); + Assert.NotNull(config); + Assert.True(config.Debug); + } +} diff --git a/Loom.Testing/DiagnosticMessageTest.cs b/Loom.Testing/DiagnosticMessageTest.cs index aaf55c05..d3b417f0 100644 --- a/Loom.Testing/DiagnosticMessageTest.cs +++ b/Loom.Testing/DiagnosticMessageTest.cs @@ -355,4 +355,23 @@ public void ToString_NewLines_UseEnvironmentNewLine() Assert.DoesNotContain("\n", result.Replace(Environment.NewLine, "")); Assert.DoesNotContain("\r", result.Replace(Environment.NewLine, "")); } + + [Fact] + public void ToString_DebugDiagnostic_RendersCompactSingleLine() + { + var start = new Location(_testFile, 4); + var end = new Location(_testFile, 9); + var span = new LocationSpan(start, end); + var diagnostic = new Diagnostic(span, DiagnosticSeverity.Debug, null, "Declared 'x' (Variable)", null); + var result = diagnostic.ToString(); + + Assert.Equal( + $"{Colors.Magenta}{Colors.Bold}debug{Colors.Reset} {Colors.Dim}[{span}]{Colors.Reset} {Colors.Gray}Declared 'x' (Variable){Colors.Reset}", + result + ); + + Assert.DoesNotContain(Environment.NewLine, result); + Assert.DoesNotContain("╭─", result); + Assert.DoesNotContain("│", result); + } } \ No newline at end of file diff --git a/Loom.Testing/ResolverTest.cs b/Loom.Testing/ResolverTest.cs index 4761c41f..cc89a566 100644 --- a/Loom.Testing/ResolverTest.cs +++ b/Loom.Testing/ResolverTest.cs @@ -1476,4 +1476,47 @@ public void Allows_SelfAsDeclarationName() Assert.Null(model.Diagnostics.Find(d => d.Code == InternalCodes.ReservedLuauKeyword)); } #endregion ReservedLuauKeywords + + #region DebugDiagnostics + [Fact] + public void Debug_False_ProducesNoDebugDiagnostics() + { + var model = Utility.GetSemanticModel("let x = 1;", debug: false); + Assert.Null(model.Diagnostics.Find(d => d.Severity == DiagnosticSeverity.Debug)); + } + + [Fact] + public void Debug_True_ProducesConsolidatedDeclarationDiagnostic() + { + var model = Utility.GetSemanticModel("let x = 1;", debug: true); + var diag = model.Diagnostics.Find(d => d.Severity == DiagnosticSeverity.Debug && d.Message.Contains("'x'")); + + Assert.NotNull(diag); + Assert.Equal("Declared 'x' (Variable)", diag.Message); + } + + [Fact] + public void Debug_True_AppliesGlobalFlag_InDeclarationFile() + { + var model = Utility.GetSemanticModel("interface Foo;", isDeclaration: true, debug: true); + var diag = model.Diagnostics.Find(d => d.Severity == DiagnosticSeverity.Debug && d.Message.Contains("'Foo'") && d.Message.Contains("Interface")); + + Assert.NotNull(diag); + Assert.Equal("Declared 'Foo' (Interface) [global]", diag.Message); + } + + [Fact] + public void Debug_True_SetsEmitDebugDiagnosticsOnSemanticModel() + { + var model = Utility.GetSemanticModel("let x = 1;", debug: true); + Assert.True(model.EmitDebugDiagnostics); + } + + [Fact] + public void Debug_False_ClearsEmitDebugDiagnosticsOnSemanticModel() + { + var model = Utility.GetSemanticModel("let x = 1;", debug: false); + Assert.False(model.EmitDebugDiagnostics); + } + #endregion DebugDiagnostics } \ No newline at end of file diff --git a/Loom.Testing/Utility.cs b/Loom.Testing/Utility.cs index f21ec398..d46960fc 100644 --- a/Loom.Testing/Utility.cs +++ b/Loom.Testing/Utility.cs @@ -43,13 +43,13 @@ private static LuauGeneratorResult Generate(string source, bool typeCheck = fals public static ParserResult Parse(string source) => new Parser(Tokenize(source)).Parse(); public static DiagnosticBag GetParserDiagnostics(string source) => Parse(source).Diagnostics; - public static Core.Resolving.SemanticModel GetSemanticModel(string source, bool isDeclaration = false, bool disableRuntimeLib = true) + public static Core.Resolving.SemanticModel GetSemanticModel(string source, bool isDeclaration = false, bool disableRuntimeLib = true, bool debug = false) { var parserResult = Parse(source); if (isDeclaration) parserResult.Tree.File.IsDeclaration = true; - var compilationUnit = new CompilationUnit(new LoomConfig()); + var compilationUnit = new CompilationUnit(new LoomConfig { Debug = debug }); var semanticModel = new Resolver(parserResult, compilationUnit).Resolve(); semanticModel.DisableRuntimeLibraryImport = disableRuntimeLib;