diff --git a/SocietalConstructionTool/Compiler/CompilerError.cs b/SocietalConstructionTool/Compiler/CompilerError.cs index 576cadf2..5369ebf7 100644 --- a/SocietalConstructionTool/Compiler/CompilerError.cs +++ b/SocietalConstructionTool/Compiler/CompilerError.cs @@ -5,6 +5,7 @@ public class CompilerError public string Message { get; } public int? Line { get; } public int? Column { get; } + public string? Filename { get; set; } public CompilerError(string message) { @@ -24,8 +25,30 @@ public CompilerError(string message, int line, int column) Column = column; } + public CompilerError(string message, string fileName, int line, int column) + { + Message = message; + Filename = fileName; + Line = line; + Column = column; + } + public override string ToString() { + + if (Filename is not null) + { + if (Line is null) + { + return $"{Filename}: {Message}"; + } + if (Column is null) + { + return $"{Filename}, Line {Line}: {Message}"; + } + return $"{Filename}, Line {Line}, Column {Column}: {Message}"; + } + if (Line is null) { return Message; diff --git a/SocietalConstructionTool/Compiler/Typechecker/SctTableVisitor.cs b/SocietalConstructionTool/Compiler/Typechecker/SctTableVisitor.cs index 26891b70..45cfa04d 100644 --- a/SocietalConstructionTool/Compiler/Typechecker/SctTableVisitor.cs +++ b/SocietalConstructionTool/Compiler/Typechecker/SctTableVisitor.cs @@ -2,30 +2,19 @@ namespace Sct.Compiler.Typechecker { - public class SctTableVisitor : SctBaseVisitor, IErrorReporter + public class SctTableVisitor(CTableBuilder cTableBuilder) : SctBaseVisitor, IErrorReporter { private CTable? InternalCTable { get; set; } public CTable CTable => InternalCTable ?? _ctableBuilder.BuildCtable(); private readonly List _errors = new(); public IEnumerable Errors => _errors; - private readonly CTableBuilder _ctableBuilder = new(); + private readonly CTableBuilder _ctableBuilder = cTableBuilder; public override SctType VisitStart([NotNull] SctParser.StartContext context) { _ = base.VisitStart(context); - InternalCTable = _ctableBuilder.BuildCtable(); - - var setupType = InternalCTable.GlobalClass.LookupFunctionType("Setup"); - if (setupType is null) - { - _errors.Add(new CompilerError("No setup function found")); - } - else if (setupType.ReturnType != TypeTable.Void || setupType.ParameterTypes.Count != 0) - { - _errors.Add(new CompilerError("Setup function must return void and take no arguments")); - } return TypeTable.None; } diff --git a/SocietalConstructionTool/SctRunner.cs b/SocietalConstructionTool/SctRunner.cs index 37c5a8f1..00375905 100644 --- a/SocietalConstructionTool/SctRunner.cs +++ b/SocietalConstructionTool/SctRunner.cs @@ -20,45 +20,116 @@ public static class SctRunner * * Reads an SCT source file, statically chekcs it and translates it into C# code * - * The path of the SCT source file + * The path of the SCT source file * The resulting C# source, or null if compilation failed */ - public static (string? outputText, IEnumerable errors) CompileSct(string filename) + public static (string? outputText, IEnumerable errors) CompileSct(string[] filenames) { - // TODO: Add error handling - string input = File.ReadAllText(filename); - ICharStream stream = CharStreams.fromString(input); - ITokenSource lexer = new SctLexer(stream); - ITokenStream tokens = new CommonTokenStream(lexer); - SctParser parser = new(tokens); - var startNode = parser.start(); - KeywordContextCheckVisitor keywordChecker = new(); - var errors = startNode.Accept(keywordChecker).ToList(); + // Make SctTableVisitor take a CTableBuilder as a parameter + // Analyse each file separately + // Add file name to each found error. + // Call CTabelBuilder.BuildCtable() after all files have been visited + // Run the translator on all files concatenated. + // Create a CTableBuilder that is used for all files. + CTableBuilder cTableBuilder = new(); + var errors = new List(); - // Run visitor that populates the tables. - var sctTableVisitor = new SctTableVisitor(); - _ = startNode.Accept(sctTableVisitor); - var ctable = sctTableVisitor.CTable; - errors.AddRange(sctTableVisitor.Errors); + // Store parses for each file to avoid having to recreate them for type checking. + Dictionary startNodes = new(); - // Run visitor that checks the types. - var sctTypeChecker = new SctTypeChecker(ctable); - _ = startNode.Accept(sctTypeChecker); - parser.Reset(); + // Run static analysis on each file separately. + foreach (var file in filenames) + { + string input = File.ReadAllText(file); + ICharStream fileStream = CharStreams.fromString(input); + ITokenSource fileLexer = new SctLexer(fileStream); + ITokenStream fileTokens = new CommonTokenStream(fileLexer); - errors.AddRange(sctTypeChecker.Errors); + var fileParser = new SctParser(fileTokens); + // Save parser for later use. + startNodes[file] = fileParser.start(); + var startNode = startNodes[file]; - var translator = new SctTranslator(); - parser.AddParseListener(translator); - _ = parser.start(); + KeywordContextCheckVisitor keywordChecker = new(); + + // Annotate each error with the filename. + var keywordErrors = startNode.Accept(keywordChecker).ToList(); + foreach (var error in keywordErrors) + { + error.Filename = file; + } + errors.AddRange(keywordErrors); + + SctReturnCheckVisitor returnChecker = new(); + _ = startNode.Accept(returnChecker); + foreach (var error in returnChecker.Errors) + { + error.Filename = file; + } + errors.AddRange(returnChecker.Errors); + + // Run visitor that populates the tables using the CTableBuilder. + var sctTableVisitor = new SctTableVisitor(cTableBuilder); + _ = startNode.Accept(sctTableVisitor); + + foreach (var error in sctTableVisitor.Errors) + { + error.Filename = file; + } + + errors.AddRange(sctTableVisitor.Errors); + } + + // Build the CTable after all files have been visited. + // The CTable is used for type checking. + CTable cTable = cTableBuilder.BuildCtable(); + + var setupType = cTable.GlobalClass.LookupFunctionType("Setup"); + if (setupType is null) + { + errors.Add(new CompilerError("No setup function found")); + } + else if (setupType.ReturnType != TypeTable.Void || setupType.ParameterTypes.Count != 0) + { + errors.Add(new CompilerError("Setup function must return void and take no arguments")); + } + + // Typecheck each file separately. + // Identifiers from other files are known because the CTable is built from all files. + foreach (var file in filenames) + { + var startNode = startNodes[file]; + + // Run visitor that checks the types. + var sctTypeChecker = new SctTypeChecker(cTable); + _ = startNode.Accept(sctTypeChecker); + + foreach (var error in sctTypeChecker.Errors) + { + error.Filename = file; + } + + errors.AddRange(sctTypeChecker.Errors); + } if (errors.Count > 0) { return (null, errors); } + // Concatenate all files into one string and run the translator on it. + string fullInput = ConcatenateFiles(filenames); + ICharStream stream = CharStreams.fromString(fullInput); + ITokenSource lexer = new SctLexer(stream); + ITokenStream tokens = new CommonTokenStream(lexer); + SctParser parser = new(tokens); + + var translator = new SctTranslator(); + parser.AddParseListener(translator); + _ = parser.start(); + if (translator.Root is null) { throw new InvalidOperationException("Translation failed"); @@ -144,10 +215,10 @@ private static void Run(Assembly assembly, IRuntimeContext initialContext) */ public static void CompileAndRun(string[] filenames, IOutputLogger logger) { - // TODO: Actually concatenate the files. Isak is working on this. - var filename = filenames[0]; - var (outputText, errors) = CompileSct(filename); + var (outputText, errors) = CompileSct(filenames); + + // TODO: Handle errors from ANTLR. They are not currently being passed to the errors list. if (errors.Any() || outputText is null) { Console.Error.WriteLine("Compilation failed:"); @@ -169,5 +240,17 @@ public static void CompileAndRun(string[] filenames, IOutputLogger logger) Run(assembly, logger); } + + private static string ConcatenateFiles(string[] filenames) + { + + string result = string.Empty; + foreach (var file in filenames) + { + result += File.ReadAllText(file); + } + + return result; + } } } diff --git a/SocietalConstructionToolTests/Snapshots/SplitFileTests/MultiFileOne.verified.txt b/SocietalConstructionToolTests/Snapshots/SplitFileTests/MultiFileOne.verified.txt new file mode 100644 index 00000000..02220488 --- /dev/null +++ b/SocietalConstructionToolTests/Snapshots/SplitFileTests/MultiFileOne.verified.txt @@ -0,0 +1,59 @@ +namespace SctGenerated +{ + using Sct.Runtime; + using System; + using System.Collections.Generic; + + public class GlobalClass + { + public class __sct_Town : BaseAgent + { + private int __sct_id { get => Fields["__sct_id"]; set => Fields["__sct_id"] = value; } + private int __sct_space { get => Fields["__sct_space"]; set => Fields["__sct_space"] = value; } + + public __sct_Town(String state, IDictionary fields) : base(state, fields) + { + } + + private bool __sct_Growing(IRuntimeContext ctx) + { + if (1 != 0) + { + Enter(ctx, "__sct_End"); + return true; + } + + Enter(ctx, "__sct_Growing"); + return true; + return false; + } + + private bool __sct_End(IRuntimeContext ctx) + { + ctx.ExitRuntime(); + return true; + return false; + } + + public override void Update(IRuntimeContext ctx) + { + _ = State switch + { + "__sct_Growing" => __sct_Growing(ctx), + "__sct_End" => __sct_End(ctx)}; + } + } + + public static void __sct_Setup(IRuntimeContext ctx) + { + ctx.AgentHandler.CreateAgent(new __sct_Town("__sct_Growing", new Dictionary(new KeyValuePair[] { new KeyValuePair("__sct_id", 1), new KeyValuePair("__sct_space", 50) }))); + } + + public static void RunSimulation(IRuntimeContext ctx) + { + Runtime runtime = new Runtime(); + __sct_Setup(ctx); + runtime.Run(ctx); + } + } +} \ No newline at end of file diff --git a/SocietalConstructionToolTests/SplitFileTests.cs b/SocietalConstructionToolTests/SplitFileTests.cs new file mode 100644 index 00000000..0ae07963 --- /dev/null +++ b/SocietalConstructionToolTests/SplitFileTests.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; + +using Sct; + +namespace SocietalConstructionToolTests +{ + [TestClass] + public class SplitFileTests : AbstractSnapshotTests + { + private static new IEnumerable Files => + Directory.GetFiles(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "SplitFileTests")) + .Select(f => new[] { f }); + + + [DataTestMethod] + public async Task RunFiles() + { + UseProjectRelativeDirectory("Snapshots/SplitFileTests"); // save snapshots here + + var files = GetFiles(); + + var (outputText, errors) = SctRunner.CompileSct(files); + + Assert.IsTrue(errors.Count() == 0, string.Join("\n", errors)); + Assert.IsNotNull(outputText); + _ = await Verify(outputText) + .UseFileName(Path.GetFileNameWithoutExtension(files[0])); + } + + // RunFiles is run for each file, and passing files to CompileSct only passes the file that triggered the test. + // This method is used to get all files to pass to CompileSct. + private static string[] GetFiles() + { + return Files.SelectMany(f => f).ToArray(); + } + } +} diff --git a/SocietalConstructionToolTests/TestFiles/SplitFileTests/MultiFileOne.sct b/SocietalConstructionToolTests/TestFiles/SplitFileTests/MultiFileOne.sct new file mode 100644 index 00000000..ad0dd09d --- /dev/null +++ b/SocietalConstructionToolTests/TestFiles/SplitFileTests/MultiFileOne.sct @@ -0,0 +1,5 @@ +function Setup() -> void { + // Create a town in the state growing with the id of 1 and space for 50 people + create Town::Growing(id: 1, space: 50); + +} diff --git a/SocietalConstructionToolTests/TestFiles/SplitFileTests/MultiFileTwo.sct b/SocietalConstructionToolTests/TestFiles/SplitFileTests/MultiFileTwo.sct new file mode 100644 index 00000000..b7576ae1 --- /dev/null +++ b/SocietalConstructionToolTests/TestFiles/SplitFileTests/MultiFileTwo.sct @@ -0,0 +1,11 @@ +class Town(int id, int space) { + state Growing { + if (1) { + enter End; + } + enter Growing; + } + state End { + exit; + } +} diff --git a/SocietalConstructionToolTests/TypecheckerTests.cs b/SocietalConstructionToolTests/TypecheckerTests.cs index f65874c9..0ced3f81 100644 --- a/SocietalConstructionToolTests/TypecheckerTests.cs +++ b/SocietalConstructionToolTests/TypecheckerTests.cs @@ -37,9 +37,22 @@ public async Task TypecheckFile(string testFile) _ = startNode.Accept(returnChecker); errors.AddRange(returnChecker.Errors); - var sctTableVisitor = new SctTableVisitor(); + var cTableBuilder = new CTableBuilder(); + + var sctTableVisitor = new SctTableVisitor(cTableBuilder); _ = sctTableVisitor.Visit(startNode); - var ctable = sctTableVisitor.CTable; + var ctable = cTableBuilder.BuildCtable(); + + var setupType = ctable.GlobalClass.LookupFunctionType("Setup"); + if (setupType is null) + { + errors.Add(new CompilerError("No setup function found")); + } + else if (setupType.ReturnType != TypeTable.Void || setupType.ParameterTypes.Count != 0) + { + errors.Add(new CompilerError("Setup function must return void and take no arguments")); + } + errors.AddRange(sctTableVisitor.Errors); var sctTypeChecker = new SctTypeChecker(ctable);