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))
{