Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,30 @@ raise AssertionError("a write-only property could be read")
});
}

/// <summary>
/// Tests that a function assigning to a module-level field declares it <c>global</c>, so the
/// write reaches the module instead of raising <c>UnboundLocalError</c> (#296).
/// </summary>
[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}");
});
}

/// <summary>
/// Builds a file whose declarations are each written over themselves.
/// </summary>
Expand All @@ -376,4 +400,26 @@ private static SourceFile Exemplar()

return file;
}

/// <summary>
/// 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).
/// </summary>
/// <returns>The file to generate; run, it exits with 2.</returns>
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;
}
}
52 changes: 52 additions & 0 deletions Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -764,13 +764,65 @@ private static ClassDeclaration DescribesPoint()
return specialisation;
}

/// <summary>
/// Tests that a module-level field a function assigns to is a <c>static mut</c> reached through
/// <c>unsafe</c>, so the program compiles and the writes stick (#296).
/// </summary>
[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);
});
}

/// <summary>
/// Gets a string the holder does not own, which is the one Rust asks for wherever it is only read.
/// </summary>
/// <returns>The type.</returns>
private static TypeReference BorrowedString() =>
new("str") { Indirection = TypeIndirection.Reference, IsReadOnly = true };

/// <summary>
/// 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).
/// </summary>
/// <returns>The file to generate; run, it exits with 2.</returns>
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;
}

/// <summary>
/// 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
Expand Down
60 changes: 60 additions & 0 deletions Coder/Languages/PythonGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ public class PythonGenerator : StandardLanguageGenerator
{
private HashSet<FieldDeclaration>? instanceInitialisedFields;

/// <summary>
/// The root <see cref="writtenModuleFields"/> was worked out for.
/// </summary>
private AstNode? writtenModuleFieldsRoot;

/// <summary>
/// The module-level fields something in the document assigns to.
/// </summary>
private HashSet<string> writtenModuleFields = new(StringComparer.Ordinal);

/// <summary>
/// Gets the unique identifier for this language generator.
/// </summary>
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -492,6 +503,52 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo
code.WriteLine();
}

/// <summary>
/// Declares <c>global</c> the module-level fields a body assigns to.
/// </summary>
/// <param name="body">The statements of the body about to be written.</param>
/// <param name="parameters">The parameters the body can see, which shadow a module field of the same name.</param>
/// <param name="code">The writer to emit into.</param>
/// <remarks>
/// 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 <c>count += 1</c> finds an
/// unbound local and raises <c>UnboundLocalError</c>. A <c>global</c> 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.
/// </remarks>
private void WriteGlobalDeclarations(IEnumerable<AstNode> body, IEnumerable<Parameter> parameters, CodeBlocker code)
{
if (!ReferenceEquals(writtenModuleFieldsRoot, GenerationRoot))
{
writtenModuleFieldsRoot = GenerationRoot;
writtenModuleFields = WrittenFileScopeFields();
}

if (writtenModuleFields.Count == 0)
{
return;
}

AstNode[] nodes = [.. body.SelectMany(SelfAndDescendants)];
HashSet<string> shadowed = new(
parameters.Select(parameter => parameter.Name).OfType<string>()
.Concat(nodes.OfType<VariableDeclaration>().Select(declaration => declaration.Name)),
StringComparer.Ordinal);

List<string> globals = [.. nodes
.OfType<AssignmentStatement>()
.Select(assignment => assignment.Target)
.OfType<VariableReference>()
.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)}");
}
}

/// <inheritdoc/>
protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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");
Expand Down
110 changes: 107 additions & 3 deletions Coder/Languages/RustGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@
/// </remarks>
private IReadOnlySet<string> reassignedParameters = new HashSet<string>(StringComparer.Ordinal);

/// <summary>
/// The parameters and locals the function being written has bound so far.
/// </summary>
/// <remarks>
/// A local or parameter named like a mutable static shadows it, and is reached without
/// <c>unsafe</c>. Cleared once each body has been written.
/// </remarks>
private readonly HashSet<string> boundLocals = new(StringComparer.Ordinal);

/// <summary>
/// The root <see cref="writtenStatics"/> was worked out for.
/// </summary>
private AstNode? writtenStaticsRoot;

/// <summary>
/// The module-level fields something in the document assigns to, which Rust declares
/// <c>static mut</c>.
/// </summary>
private HashSet<string> writtenStatics = new(StringComparer.Ordinal);

/// <summary>
/// Whether the code being written is already inside an <c>unsafe</c> block.
/// </summary>
private bool insideUnsafe;

/// <summary>
/// Spells type parameters where they are being declared, bounds and all.
/// </summary>
Expand Down Expand Up @@ -386,7 +411,7 @@
/// The associated constants of <c>f64</c>, which need nothing imported.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "f64::NAN" : value > 0 ? "f64::INFINITY" : "f64::NEG_INFINITY";

Check warning on line 414 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand All @@ -394,7 +419,7 @@
/// <c>f32</c> implicitly.
/// </remarks>
protected override string SpellNonFiniteSingle(float value) =>
float.IsNaN(value) ? "f32::NAN" : value > 0 ? "f32::INFINITY" : "f32::NEG_INFINITY";

Check warning on line 422 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand Down Expand Up @@ -852,7 +877,7 @@
/// <param name="funcDecl">The declaration to emit.</param>
/// <param name="typeName">The type it belongs to.</param>
/// <param name="code">The writer to emit into.</param>
private void GenerateTraitImplementation(FunctionDeclaration funcDecl, string typeName, CodeBlocker code)

Check warning on line 880 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.
{
if (funcDecl.Kind == FunctionKind.ConversionOperator)
{
Expand Down Expand Up @@ -904,7 +929,7 @@
.. operands.Select(parameter => new KeyValuePair<string, TypeReference?>(OperandName(parameter), parameter.Type)),
]);
bool rebind = receiverName != SelfName;
string self = op.ReturnsBool ? "&self" : reassigned.Contains(receiverName) && !rebind ? $"mut {SelfName}" : SelfName;

Check warning on line 932 in Coder/Languages/RustGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.
code.Write($"fn {op.Method}({self}");

foreach (Parameter parameter in operands)
Expand Down Expand Up @@ -1270,9 +1295,73 @@
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);
}

/// <inheritdoc/>
/// <remarks>
/// A read of a <c>static mut</c> is unsafe as well as a write, so outside a block that is
/// already unsafe it is written as an <c>unsafe</c> expression that yields the value.
/// </remarks>
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);
}

/// <summary>
/// Tells whether a name, as a body sees it, is a module-level field declared <c>static mut</c>.
/// </summary>
/// <param name="name">The name to look up.</param>
/// <returns>Whether it names a mutable static rather than a local, parameter or constant.</returns>
private bool IsMutableStatic(string name) =>
!boundLocals.Contains(name) && WrittenStatics().Contains(name);

/// <summary>
/// Reads the module-level fields written to in the document being generated, once per document.
/// </summary>
/// <returns>Their names.</returns>
private HashSet<string> WrittenStatics()
{
if (!ReferenceEquals(writtenStaticsRoot, GenerationRoot))
{
writtenStaticsRoot = GenerationRoot;
writtenStatics = WrittenFileScopeFields();
}

return writtenStatics;
}

/// <summary>
/// How a body names a field of the instance its function was called on.
/// </summary>
Expand Down Expand Up @@ -1323,9 +1412,16 @@
/// <param name="code">The writer to emit into.</param>
private void WriteBody(IEnumerable<AstNode> statements, CodeBlocker code)
{
foreach (AstNode statement in statements)
try
{
GenerateInternal(statement, code);
foreach (AstNode statement in statements)
{
GenerateInternal(statement, code);
}
}
finally
{
boundLocals.Clear();
}
}

Expand Down Expand Up @@ -1678,6 +1774,11 @@
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)));

Expand Down Expand Up @@ -1720,6 +1821,7 @@

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
Expand Down Expand Up @@ -1785,7 +1887,9 @@
}

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";

Check warning on line 1892 in Coder/Languages/RustGenerator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaFXYGoTOhjqON2Dyfc6&open=AaFXYGoTOhjqON2Dyfc6&pullRequest=319

code.Write($"{visibility}{storage} ");
code.Write($"{EscapeIdentifier(field.Name ?? string.Empty)}: {SpellStorageType(type)} = ");
Expand Down
Loading
Loading