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
90 changes: 90 additions & 0 deletions Coder.Test/Languages/RustGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,96 @@ public void StringLiteralsReceivedAsOwnedStrings_Compile()
});
}

/// <summary>
/// Tests that a string literal compiles in every other place an owned <c>String</c> receives one:
/// a constant, a constructor's initialiser, a struct literal's field, an assignment to a field,
/// and both branches of a conditional answering one.
/// </summary>
/// <remarks>
/// <c>String::from</c> is not a <c>const fn</c>, so a constant cannot be an owned <c>String</c>
/// at all: it is a <c>&amp;'static str</c>, and a table of them a slice of those.
/// </remarks>
[TestMethod]
public void StringLiteralsFillingStringFieldsAndConstants_Compile()
{
if (ToolchainHarness.FindOnPath("--version", "rustc") is null)
{
Assert.Inconclusive("No Rust compiler on the path, so nothing was compiled.");
return;
}

ClassDeclaration person = new("Person") { Kind = TypeDeclarationKind.Struct };
person.Members.Add(new VariableDeclaration("name", "string"));
person.Members.Add(new FieldDeclaration("nickname", "str") { InitialValue = Literal.Text("none") });
person.Members.Add(new FieldDeclaration("DEFAULT", "string")
{
IsStatic = true,
IsConstant = true,
InitialValue = Literal.Text("d"),
});

FunctionDeclaration ctor = new("Person") { Kind = FunctionKind.Constructor };
ctor.Initialisers.Add(new MemberInitialiser("name", Literal.Text("Anon")));
person.Members.Add(ctor);

ConstructionExpression built = new(new TypeReference("Person"));
built.Arguments.Add(new MemberInitialiser("name", Literal.Text("x")));
built.Arguments.Add(new MemberInitialiser("nickname", Literal.Text("y")));
FunctionDeclaration make = new("make") { ReturnType = "Person", IsStatic = true };
make.Body.Add(new ReturnStatement(built));
person.Members.Add(make);

FunctionDeclaration rename = new("rename");
rename.Body.Add(new AssignmentStatement(new VariableReference("self.name"), Literal.Text("y")));
person.Members.Add(rename);

FunctionDeclaration pick = new("pick") { ReturnType = "string", IsStatic = true };
pick.Parameters.Add(new Parameter("c", "bool"));
pick.Body.Add(new ReturnStatement(new ConditionalExpression(new VariableReference("c"), Literal.Text("a"), Literal.Text("b"))));
person.Members.Add(pick);

// A struct built outside its own impl, so the field types come from the declaration rather
// than from the type whose functions are being written.
ConstructionExpression stranger = new(new TypeReference("Person"));
stranger.Arguments.Add(new MemberInitialiser("name", Literal.Text("z")));
stranger.Arguments.Add(new MemberInitialiser("nickname", Literal.Text("w")));
FunctionDeclaration meet = new("meet") { ReturnType = "Person" };
meet.Body.Add(new ReturnStatement(stranger));

ConstructionExpression names = new();
names.Arguments.Add(Literal.Text("a"));
names.Arguments.Add(Literal.Text("b"));

SourceFile file = new("owned_strings");
file.Members.Add(person);
file.Members.Add(meet);
file.Members.Add(new FieldDeclaration("GREETING", "string") { IsStatic = true, IsConstant = true, InitialValue = Literal.Text("hi") });
file.Members.Add(new FieldDeclaration("MOTTO", "str") { IsStatic = true, InitialValue = Literal.Text("go") });
file.Members.Add(new FieldDeclaration("NAMES", new TypeReference("string") { IsArray = true })
{
IsStatic = true,
IsConstant = true,
InitialValue = names,
});

ToolchainHarness.InTemporaryDirectory(directory =>
{
string source = new RustGenerator().Generate(file);
string allowed = "#![allow(unused_parens, dead_code)]";

File.WriteAllText(
Path.Combine(directory, "owned_strings.rs"),
$"{allowed}{Environment.NewLine}{source}");

(int exitCode, string output) = ToolchainHarness.Run(
"rustc",
"--crate-type lib --edition 2021 -o owned_strings.rlib owned_strings.rs",
directory);

Assert.AreEqual(0, exitCode, $"rustc rejected the generated source:{Environment.NewLine}{source}{Environment.NewLine}{output}");
});
}

/// <summary>
/// Tests that bitwise complement and unary plus compile: Rust has neither <c>~</c> nor a unary
/// <c>+</c>, so both have to be spelled its way.
Expand Down
142 changes: 132 additions & 10 deletions Coder/Languages/RustGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,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 344 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.

Check warning on line 344 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.

Check warning on line 344 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 @@ -349,7 +349,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 352 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.

Check warning on line 352 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.

Check warning on line 352 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 @@ -808,7 +808,7 @@
new(SelfName, new TypeReference(typeName)),
.. funcDecl.Parameters.Select(parameter => new KeyValuePair<string, TypeReference?>(OperandName(parameter), parameter.Type)),
]);
string self = op.ReturnsBool ? "&self" : reassigned.Contains(SelfName) ? $"mut {SelfName}" : SelfName;

Check warning on line 811 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.

Check warning on line 811 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 funcDecl.Parameters)
Expand Down Expand Up @@ -1038,12 +1038,25 @@
/// map to the owned <c>String</c>, so a literal received as one is written
/// <c>String::from("…")</c>. Rust converts neither way on its own. Where the receiving type is
/// borrowed, or not declared, the literal stays bare, which is what those positions want.
/// <para>
/// A conditional answers what its branches do, so each branch is written for the same type.
/// </para>
/// </remarks>
private void WriteValue(AstNode value, TypeReference? target, CodeBlocker code)
{
bool owned = value is LiteralExpression<string>
&& target is not null
&& SpellType(target) == TypeMappings["string"];
if (value is ConditionalExpression conditional && IsOwnedString(target))
{
code.Write("(if ");
GenerateInternal(conditional.Condition, code);
code.Write(" { ");
WriteValue(conditional.WhenTrue, target, code);
code.Write(" } else { ");
WriteValue(conditional.WhenFalse, target, code);
code.Write(" })");
return;
}

bool owned = value is LiteralExpression<string> && IsOwnedString(target);

if (owned)
{
Expand All @@ -1058,6 +1071,74 @@
}
}

/// <inheritdoc/>
/// <remarks>
/// A value assigned to a field of <c>self</c> is written for the type the field was declared
/// with, so a string literal assigned to a String field is made into one.
/// </remarks>
protected override void GenerateAssignmentStatement(AssignmentStatement assignment, CodeBlocker code)
{
Ensure.NotNull(assignment);
Ensure.NotNull(code);

if (assignment.Operator == AssignmentOperator.Assign
&& assignment.Target is VariableReference { Name: string target }
&& target.StartsWith(SelfFieldPrefix, StringComparison.Ordinal)
&& InstanceFieldType(constructedType, target[SelfFieldPrefix.Length..]) is TypeReference fieldType)
{
GenerateInternal(assignment.Target, code);
code.Write($" {GetAssignmentOperator(AssignmentOperator.Assign)} ");
WriteValue(assignment.Value, fieldType, code);
EndStatement(code);
return;
}

base.GenerateAssignmentStatement(assignment, code);
}

/// <summary>
/// How a body names a field of the instance its function was called on.
/// </summary>
private const string SelfFieldPrefix = "self.";

/// <summary>
/// Reports whether a type is spelled as Rust's owned <c>String</c>.
/// </summary>
/// <param name="type">The type, or null where none is declared.</param>
/// <returns>True for <c>str</c> and <c>string</c> as a plain value.</returns>
private static bool IsOwnedString(TypeReference? type) =>
type is not null && SpellType(type) == TypeMappings["string"];

/// <summary>
/// Finds the declared type of an instance field of a type.
/// </summary>
/// <param name="type">The type, or null where it is not declared in the document.</param>
/// <param name="name">The field's name.</param>
/// <returns>The field's type, or null where the field or its type is not declared.</returns>
private static TypeReference? InstanceFieldType(ClassDeclaration? type, string? name) =>
type?.Members
.Select(member => member switch
{
FieldDeclaration field when !IsAssociatedField(field) && field.Name == name => field.Type,
VariableDeclaration field when field.Name == name => field.Type,
_ => null,
})
.FirstOrDefault(found => found is not null);

/// <summary>
/// Finds the declaration of a type a struct literal builds.
/// </summary>
/// <param name="type">The type the literal names.</param>
/// <returns>The declaration, or null where the document does not declare it.</returns>
/// <remarks>
/// <c>Self</c> is the type whose functions are being written. Any other name is looked up in the
/// document, so a literal built outside the type's own impl is written for its fields too.
/// </remarks>
private ClassDeclaration? BuiltType(TypeReference type) =>
type.Name == "Self" || type.Name == constructedType?.Name
? constructedType
: DeclaredTypes(GenerationRoot).FirstOrDefault(declared => declared.Name == type.Name);

/// <summary>
/// Writes a function's statements.
/// </summary>
Expand Down Expand Up @@ -1450,7 +1531,7 @@

bool inferred = varDecl.IsTypeInferred || varDecl.Type is null;
TypeReference? annotation = !inferred ? varDecl.Type
: varDecl.InitialValue is null ? new TypeReference(UnknownTypeName)

Check warning on line 1534 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.

Check warning on line 1534 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.
: InferredLiteralWidth(varDecl.InitialValue);

if (annotation is not null)
Expand Down Expand Up @@ -1533,16 +1614,20 @@
/// </summary>
/// <param name="type">The declared type.</param>
/// <returns>The type as Rust writes it in that position.</returns>
/// <remarks>
/// A string is the borrowed <c>&amp;'static str</c> rather than the owned <c>String</c> it is
/// everywhere else: <c>String::from</c> is not a <c>const fn</c>, so no constant or static can
/// hold a <c>String</c> built from a literal, and the literal itself is a <c>&amp;'static str</c>.
/// The lifetime is spelled because an associated constant may not leave it out.
/// </remarks>
private static string SpellStorageType(TypeReference type)
{
if (!type.IsArray)
{
return SpellType(type);
}

TypeReference element = type.Clone();
element.IsArray = false;
return $"&[{SpellType(element)}]";

string spelled = IsOwnedString(element) ? "&'static str" : SpellType(element);

return type.IsArray ? $"&[{spelled}]" : spelled;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -1681,14 +1766,31 @@
if (construction.Arguments.Any(argument => argument is MemberInitialiser))
{
code.Write($"{type} ");
WriteElementList(construction, code, "{", "}", "{}");

ClassDeclaration? outerLiteral = literalType;
literalType = BuiltType(construction.Type);
try
{
WriteElementList(construction, code, "{", "}", "{}");
}
finally
{
literalType = outerLiteral;
}

return;
}

code.Write(type);
WriteCallArguments(construction, code);
}

/// <summary>
/// The type whose struct literal is being written, where the document declares it, which says
/// what each field named in the literal receives.
/// </summary>
private ClassDeclaration? literalType;

/// <summary>
/// Writes the arguments of a construction that names none of its members.
/// </summary>
Expand Down Expand Up @@ -1733,6 +1835,26 @@
return;
}

// A field is written for the type it was declared with, so a string literal filling a
// String field is made into one, as it is where a function returns one.
if (argument is MemberInitialiser { Value: AstNode value } field
&& InstanceFieldType(literalType, field.Name) is TypeReference fieldType)
{
ClassDeclaration? outerLiteral = literalType;
literalType = null;
try
{
WriteDesignator(field.Name ?? string.Empty, code);
WriteValue(value, fieldType, code);
}
finally
{
literalType = outerLiteral;
}

return;
}

base.WriteListElement(argument, code);
}

Expand Down
Loading