diff --git a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs index c3f34cf..c2e5090 100644 --- a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs +++ b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs @@ -351,6 +351,30 @@ raise AssertionError("a write-only property could be read") }); } + /// + /// Tests that a function assigning to a module-level field declares it global, so the + /// write reaches the module instead of raising UnboundLocalError (#296). + /// + [TestMethod] + public void ModuleFieldWrittenByAFunction_RunsAndKeepsItsValue() + { + string? python = ToolchainHarness.FindOnPath("--version", "python3", "python"); + if (python is null) + { + Assert.Inconclusive("No Python interpreter on the path, so nothing was run."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + string source = new PythonGenerator().Generate(Counter()); + File.WriteAllText(Path.Combine(directory, "counter.py"), source); + + (int exitCode, string output) = ToolchainHarness.Run(python, "counter.py", directory); + Assert.AreEqual(2, exitCode, $"{output}{Environment.NewLine}{source}"); + }); + } + /// /// Builds a file whose declarations are each written over themselves. /// @@ -376,4 +400,26 @@ private static SourceFile Exemplar() return file; } + + /// + /// Builds a file whose function assigns to a module-level field and returns it, and whose entry + /// point calls it twice, exiting with what the second call returns (#296). + /// + /// The file to generate; run, it exits with 2. + private static SourceFile Counter() + { + SourceFile file = new("counter"); + file.Members.Add(new FieldDeclaration("count", new TypeReference("int")) { InitialValue = Literal.Number(0) }); + + FunctionDeclaration bump = new("bump") { ReturnType = new TypeReference("int") }; + bump.Body.Add(new AssignmentStatement(new VariableReference("count"), Literal.Number(1), AssignmentOperator.AddAssign)); + bump.Body.Add(new ReturnStatement(new VariableReference("count"))); + file.Members.Add(bump); + + EntryPoint main = new() { ReturnsExitCode = true }; + main.Body.Add(new ExpressionStatement(new CallExpression("bump"))); + main.Body.Add(new ReturnStatement(new CallExpression("bump"))); + file.Members.Add(main); + return file; + } } diff --git a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs index 5629817..773f121 100644 --- a/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs @@ -764,6 +764,36 @@ private static ClassDeclaration DescribesPoint() return specialisation; } + /// + /// Tests that a module-level field a function assigns to is a static mut reached through + /// unsafe, so the program compiles and the writes stick (#296). + /// + [TestMethod] + public void ModuleFieldWrittenByAFunction_CompilesAndKeepsItsValue() + { + if (ToolchainHarness.FindOnPath("--version", "rustc") is null) + { + Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + string program = OperatingSystem.IsWindows() ? "counter.exe" : "counter"; + string source = new RustGenerator().Generate(Counter()); + File.WriteAllText(Path.Combine(directory, "counter.rs"), source); + + (int built, string output) = ToolchainHarness.Run( + "rustc", + $"--edition 2021 -o {program} counter.rs", + directory); + Assert.AreEqual(0, built, $"rustc rejected the generated source:{Environment.NewLine}{output}{Environment.NewLine}{source}"); + + (int counted, _) = ToolchainHarness.Run(Path.Combine(directory, program), string.Empty, directory); + Assert.AreEqual(2, counted, source); + }); + } + /// /// Gets a string the holder does not own, which is the one Rust asks for wherever it is only read. /// @@ -771,6 +801,28 @@ private static ClassDeclaration DescribesPoint() private static TypeReference BorrowedString() => new("str") { Indirection = TypeIndirection.Reference, IsReadOnly = true }; + /// + /// Builds a file whose function assigns to a module-level field and returns it, and whose entry + /// point calls it twice, exiting with what the second call returns (#296). + /// + /// The file to generate; run, it exits with 2. + private static SourceFile Counter() + { + SourceFile file = new("counter"); + file.Members.Add(new FieldDeclaration("count", new TypeReference("int")) { InitialValue = Literal.Number(0) }); + + FunctionDeclaration bump = new("bump") { ReturnType = new TypeReference("int") }; + bump.Body.Add(new AssignmentStatement(new VariableReference("count"), Literal.Number(1), AssignmentOperator.AddAssign)); + bump.Body.Add(new ReturnStatement(new VariableReference("count"))); + file.Members.Add(bump); + + EntryPoint main = new() { ReturnsExitCode = true }; + main.Body.Add(new ExpressionStatement(new CallExpression("bump"))); + main.Body.Add(new ReturnStatement(new CallExpression("bump"))); + file.Members.Add(main); + return file; + } + /// /// Tests that numbers of different widths meeting in an operator, an initialiser, an assignment, /// a return and a call argument compile, which Rust only allows once the narrower is converted diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs index b54c099..196f09b 100644 --- a/Coder/Languages/PythonGenerator.cs +++ b/Coder/Languages/PythonGenerator.cs @@ -22,6 +22,16 @@ public class PythonGenerator : StandardLanguageGenerator { private HashSet? instanceInitialisedFields; + /// + /// The root was worked out for. + /// + private AstNode? writtenModuleFieldsRoot; + + /// + /// The module-level fields something in the document assigns to. + /// + private HashSet writtenModuleFields = new(StringComparer.Ordinal); + /// /// Gets the unique identifier for this language generator. /// @@ -204,6 +214,7 @@ private void WriteAccessorBody(PropertyDeclaration declaration, bool isSetter, C code.WriteLine(); using IndentScope statements = new(code); + WriteGlobalDeclarations(body, [], code); if (body.Count == 0) { @@ -492,6 +503,52 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo code.WriteLine(); } + /// + /// Declares global the module-level fields a body assigns to. + /// + /// The statements of the body about to be written. + /// The parameters the body can see, which shadow a module field of the same name. + /// The writer to emit into. + /// + /// Python decides a name's scope from the whole function before running any of it: one assignment + /// anywhere makes the name local everywhere in that body, so the read in count += 1 finds an + /// unbound local and raises UnboundLocalError. A global line is what says the + /// assignment means the module's. Only fields the body itself assigns are listed, because a read + /// alone already reaches the module and a declaration nothing needs is noise. + /// + private void WriteGlobalDeclarations(IEnumerable body, IEnumerable parameters, CodeBlocker code) + { + if (!ReferenceEquals(writtenModuleFieldsRoot, GenerationRoot)) + { + writtenModuleFieldsRoot = GenerationRoot; + writtenModuleFields = WrittenFileScopeFields(); + } + + if (writtenModuleFields.Count == 0) + { + return; + } + + AstNode[] nodes = [.. body.SelectMany(SelfAndDescendants)]; + HashSet shadowed = new( + parameters.Select(parameter => parameter.Name).OfType() + .Concat(nodes.OfType().Select(declaration => declaration.Name)), + StringComparer.Ordinal); + + List globals = [.. nodes + .OfType() + .Select(assignment => assignment.Target) + .OfType() + .Select(reference => reference.Name.Split('.')[0]) + .Where(name => writtenModuleFields.Contains(name) && !shadowed.Contains(name)) + .Distinct(StringComparer.Ordinal)]; + + if (globals.Count > 0) + { + code.WriteLine($"global {string.Join(", ", globals)}"); + } + } + /// protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code) { @@ -519,6 +576,7 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl // Python's body is delimited by indentation alone, so there is no brace scope to open. using IndentScope body = new(code); + WriteGlobalDeclarations(funcDecl.Body, funcDecl.Parameters, code); // Function body if (funcDecl.Body.Count == 0) @@ -684,6 +742,7 @@ private void GenerateMethod(FunctionDeclaration method, ClassDeclaration declara WriteMethodSignature(method, code); using IndentScope body = new(code); + WriteGlobalDeclarations(method.Body, method.Parameters, code); // A method a derived class has to supply is a method whose body is a refusal. Python has no // declaration without a definition, so the definition says what calling it means. @@ -855,6 +914,7 @@ protected override void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker co using (IDisposable types = OpenStaticTypeScope(entryPoint)) using (IndentScope body = new(code)) { + WriteGlobalDeclarations(entryPoint.Body, [], code); if (entryPoint.Body.Count == 0) { code.WriteLine("pass"); diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index ab01690..ad6cf02 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -147,6 +147,31 @@ public class RustGenerator : StandardLanguageGenerator /// private IReadOnlySet reassignedParameters = new HashSet(StringComparer.Ordinal); + /// + /// The parameters and locals the function being written has bound so far. + /// + /// + /// A local or parameter named like a mutable static shadows it, and is reached without + /// unsafe. Cleared once each body has been written. + /// + private readonly HashSet boundLocals = new(StringComparer.Ordinal); + + /// + /// The root was worked out for. + /// + private AstNode? writtenStaticsRoot; + + /// + /// The module-level fields something in the document assigns to, which Rust declares + /// static mut. + /// + private HashSet writtenStatics = new(StringComparer.Ordinal); + + /// + /// Whether the code being written is already inside an unsafe block. + /// + private bool insideUnsafe; + /// /// Spells type parameters where they are being declared, bounds and all. /// @@ -1270,9 +1295,73 @@ protected override void GenerateAssignmentStatement(AssignmentStatement assignme return; } + if (assignment.Target is VariableReference { Name: string name } && IsMutableStatic(RootOf(name))) + { + // Every access to a static mut is unsafe, because nothing stops two threads reaching it at + // once. The block holds the whole statement so a value read from the same static is + // covered by it rather than needing a block of its own. + code.WriteLine("unsafe {"); + code.Indent(); + insideUnsafe = true; + try + { + base.GenerateAssignmentStatement(assignment, code); + } + finally + { + insideUnsafe = false; + } + + code.Outdent(); + code.WriteLine("}"); + return; + } + base.GenerateAssignmentStatement(assignment, code); } + /// + /// + /// A read of a static mut is unsafe as well as a write, so outside a block that is + /// already unsafe it is written as an unsafe expression that yields the value. + /// + protected override void GenerateVariableReference(VariableReference reference, CodeBlocker code) + { + Ensure.NotNull(reference); + Ensure.NotNull(code); + + if (!insideUnsafe && IsMutableStatic(RootOf(reference.Name))) + { + code.Write($"unsafe {{ {EscapeReference(reference.Name)} }}"); + return; + } + + base.GenerateVariableReference(reference, code); + } + + /// + /// Tells whether a name, as a body sees it, is a module-level field declared static mut. + /// + /// The name to look up. + /// Whether it names a mutable static rather than a local, parameter or constant. + private bool IsMutableStatic(string name) => + !boundLocals.Contains(name) && WrittenStatics().Contains(name); + + /// + /// Reads the module-level fields written to in the document being generated, once per document. + /// + /// Their names. + private HashSet WrittenStatics() + { + if (!ReferenceEquals(writtenStaticsRoot, GenerationRoot)) + { + writtenStaticsRoot = GenerationRoot; + writtenStatics = WrittenFileScopeFields(); + } + + return writtenStatics; + } + /// /// How a body names a field of the instance its function was called on. /// @@ -1323,9 +1412,16 @@ private bool IsOwnedString(TypeReference? type) => /// The writer to emit into. private void WriteBody(IEnumerable statements, CodeBlocker code) { - foreach (AstNode statement in statements) + try { - GenerateInternal(statement, code); + foreach (AstNode statement in statements) + { + GenerateInternal(statement, code); + } + } + finally + { + boundLocals.Clear(); } } @@ -1678,6 +1774,11 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, code.Write("mut "); } + if (parameter.Name is not null) + { + boundLocals.Add(parameter.Name); + } + code.Write($"{EscapeIdentifier(parameter.Name ?? $"param{position}")}: "); code.Write(SpellType(parameter.Type ?? new TypeReference(UnknownTypeName))); @@ -1720,6 +1821,7 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, code.Write(varDecl.IsConstant ? "let " : "let mut "); code.Write(EscapeIdentifier(varDecl.Name)); + boundLocals.Add(varDecl.Name); bool inferred = varDecl.IsTypeInferred || varDecl.Type is null; TypeReference? annotation = !inferred ? varDecl.Type @@ -1785,7 +1887,9 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo } string visibility = IsAssociatedItem ? string.Empty : SpellVisibilityKeyword(field.Visibility); - string storage = IsAssociatedItem || field.IsConstant ? "const" : "static"; + string storage = IsAssociatedItem || field.IsConstant ? "const" + : field.Name is not null && WrittenStatics().Contains(field.Name) ? "static mut" + : "static"; code.Write($"{visibility}{storage} "); code.Write($"{EscapeIdentifier(field.Name ?? string.Empty)}: {SpellStorageType(type)} = "); diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index f288908..cc7116b 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -512,6 +512,83 @@ or BinaryOperator.LogicalAnd return staticTypeScopes.TryPeek(out StaticTypeScope? scope) ? scope.ReturnType : null; } + /// + /// Emits a reference to a variable, field or parameter by name. + /// + /// The reference to emit. + /// The writer to emit into. + /// + /// The name is written as it stands, with any segment that is a reserved word escaped. A language + /// that reaches some names through more than the name itself, such as Rust's mutable statics, + /// overrides this. + /// + protected virtual void GenerateVariableReference(VariableReference reference, CodeBlocker code) + { + Ensure.NotNull(reference); + Ensure.NotNull(code); + code.Write(EscapeReference(reference.Name)); + } + + /// + /// Finds the file-scope fields some statement in what is being generated assigns to. + /// + /// The names of the written fields. + /// + /// A field is file-scope when it is a member of a or of a namespace in + /// one, rather than of a type. A constant is never counted: an assignment to one is the document's + /// mistake, and treating it as written would quietly make it mutable. A target that names a member + /// of the field (count.total) writes the field too, so the name it starts from is what is + /// matched. A function's own local or parameter of the same name is not told apart here; each + /// language decides what shadowing means for the names this returns. + /// + protected HashSet WrittenFileScopeFields() + { + HashSet written = new(StringComparer.Ordinal); + if (GenerationRoot is null) + { + return written; + } + + HashSet fields = new(StringComparer.Ordinal); + foreach (AstNode node in SelfAndDescendants(GenerationRoot)) + { + IEnumerable members = node switch + { + SourceFile file => file.Members, + NamespaceDeclaration space => space.Members, + _ => [], + }; + + foreach (FieldDeclaration field in members.OfType()) + { + if (!field.IsConstant && !string.IsNullOrEmpty(field.Name)) + { + fields.Add(field.Name); + } + } + } + + if (fields.Count == 0) + { + return written; + } + + foreach (AssignmentStatement assignment in SelfAndDescendants(GenerationRoot).OfType()) + { + if (assignment.Target is VariableReference { Name: string name }) + { + int dot = name.IndexOf('.', StringComparison.Ordinal); + string root = dot < 0 ? name : name[..dot]; + if (fields.Contains(root)) + { + written.Add(root); + } + } + } + + return written; + } + /// /// Reads the return type of the one function a receiver-less call can name. /// @@ -856,6 +933,10 @@ protected sealed override void GenerateInternal(AstNode node, CodeBlocker code) GenerateAssignmentStatement(assignment, code); break; + case VariableReference reference: + GenerateVariableReference(reference, code); + break; + default: if (!TryGenerateCommonNode(node, code)) {