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
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ source in seven target languages. The solution uses:
whole a pointer to the member — to exactly one of them, and Go satisfies an interface structurally
and so writes `var _ Contract = (*Type)(nil)`, an assertion the compiler checks rather than a
declaration. A generator handed one list would be guessing which entry was the class.
Python folds the two back into the one list it has, and is the one target that has to do more than
spell them: a base list is an expression evaluated as the `class` statement runs, so a base naming
the class being declared — the self-type idiom, an interface written over its own implementer —
names something that does not exist yet and raises `NameError` on import. The argument is quoted,
which is what `typing` takes as a forward reference and resolves once something asks;
`from __future__ import annotations` is the other half of the idea and does not reach a base, which
is an expression rather than an annotation. `PythonGeneratedSourceImportsTests` loads what the
generator writes, because that is a class of error no test pinning the text can see.
The three modifiers split along a line worth stating once: `IsRecord` and `IsReadOnly` are claims
about the type — it compares by value, no member of it modifies it — so a target with no word for
one writes it down, the same as `CompileTimeAssertion`, while Rust's `#[derive(Clone, Debug,
Expand Down
122 changes: 122 additions & 0 deletions Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

using ktsu.Coder.Ast;
using ktsu.Coder.Languages;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Runs what <see cref="PythonGenerator"/> writes, with a real interpreter.
/// </summary>
/// <remarks>
/// The other four targets that are checked by a toolchain are checked by compiling. Python has no
/// compiler, and the equivalent question is whether the module can be imported at all: a Python file
/// is a program that builds its own declarations, so a class statement runs when the module is loaded
/// and anything wrong with it is raised then. That is the whole class of error a test pinning the
/// text cannot see — the spelling looks right and the interpreter disagrees — and a base list naming
/// the class being declared is exactly one of them.
/// <para>
/// The driver is written the way a consumer would be: it supplies the names the generated module
/// expects to find — the interface it implements, and the type variable it is written over — and then
/// runs the module against them. <c>runpy</c> is how a module is run with names already in its
/// namespace; an import cannot, and the AST has no way to declare a <c>TypeVar</c> or a
/// <c>Generic</c> for the file to carry its own.
/// </para>
/// <para>
/// The test is inconclusive rather than failing where no interpreter is on the path, which is the
/// honest result: nothing was run.
/// </para>
/// </remarks>
[TestClass]
public class PythonGeneratedSourceImportsTests
{
/// <summary>
/// The consumer of the generated module, written the way a person would write one.
/// </summary>
/// <remarks>
/// Every declaration the module makes is asked for afterwards, so a module that loads but declares
/// nothing fails here rather than passing quietly. <c>Length</c> is the self-type idiom and
/// <c>Node</c> the mutually-referential pair, which are the two shapes a base list can hold a name
/// that does not exist yet.
/// </remarks>
private const string Driver = """
import runpy
from typing import Generic, TypeVar

T = TypeVar("T")
TSelf = TypeVar("TSelf")


class IVector0(Generic[TSelf, T]):
pass


class Visitor(Generic[TSelf]):
pass


module = runpy.run_path(
"exemplar.py",
init_globals={"IVector0": IVector0, "Visitor": Visitor, "T": T},
)

assert module["Length"].__name__ == "Length"
assert module["Node"].__name__ == "Node"

""";

/// <summary>
/// Tests that a module whose classes are written over themselves can be loaded.
/// </summary>
[TestMethod]
public void GeneratedSource_Imports()
{
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 =>
{
File.WriteAllText(Path.Combine(directory, "driver.py"), Driver);
File.WriteAllText(
Path.Combine(directory, "exemplar.py"),
new PythonGenerator().Generate(Exemplar()));

(int exitCode, string output) = ToolchainHarness.Run(python, "driver.py", directory);
Assert.AreEqual(
0,
exitCode,
$"Python could not load the generated module:{Environment.NewLine}{output}");
});
}

/// <summary>
/// Builds a file whose declarations are each written over themselves.
/// </summary>
/// <returns>The file to generate.</returns>
private static SourceFile Exemplar()
{
SourceFile file = new("exemplar");
file.HeaderComment.Add("Generated by Coder. Do not edit.");

// The shape every generated ktsu.Semantics quantity has: an interface written over the type
// implementing it, so that the method it declares answers that type rather than the interface.
ClassDeclaration length = new("Length");
length.Interfaces.Add(TypeReference.Parse("IVector0<Length<T>, T>"));
length.Members.Add(new VariableDeclaration("Value", "T"));

// The same knot without generics of its own: a type and the thing that visits it, each named
// in the other's declaration.
ClassDeclaration node = new("Node");
node.Interfaces.Add(TypeReference.Parse("Visitor<Node>"));

file.Members.Add(length);
file.Members.Add(node);

return file;
}
}
7 changes: 6 additions & 1 deletion Coder.Test/Languages/ToolchainHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ namespace ktsu.Coder.Test.Languages;
using System.Diagnostics;

/// <summary>
/// Runs a real compiler over generated source.
/// Runs a real toolchain over generated source.
/// </summary>
/// <remarks>
/// Three of the generators here are checked by compiling what they write, because the rules they
/// have to obey are not visible in the text: C's about linkage and constant expressions, Rust's
/// about receivers, associated items and what a trait implementation owes its trait, and Go's about
/// unused names, which it refuses rather than warns about. Everything those tests share about
/// *running* a compiler is here, so that what is left in each of them is the language.
/// <para>
/// Python is asked the same question by an interpreter rather than a compiler, having no compiler to
/// ask: a Python file builds its own declarations as it is loaded, so a declaration the language
/// refuses raises on import rather than failing to build.
/// </para>
/// </remarks>
internal static class ToolchainHarness
{
Expand Down
89 changes: 89 additions & 0 deletions Coder.Test/Languages/TypeDeclarationShapeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ private static ClassDeclaration Money() =>
IsReadOnly = true,
};

/// <summary>
/// A type implementing an interface written over the type itself, which is how an interface gives
/// a method the implementing type as its result — <c>TSelf Create(T value)</c> rather than
/// <c>IVector0 Create(T value)</c>.
/// </summary>
/// <returns>The declaration.</returns>
private static ClassDeclaration SelfTyped()
{
ClassDeclaration length = new("Length");
length.Interfaces.Add(TypeReference.Parse("IVector0<Length<T>, T>"));
return length;
}

private static string Generate(ILanguageGenerator generator, AstNode node) => generator.Generate(node);

// ------------------------------------------------------------------ Interfaces
Expand Down Expand Up @@ -138,6 +151,82 @@ public void Python_TakesThemAllAsBases()
StringAssert.Contains(code, "class Widget(Control, Drawable, Clickable):");
}

/// <summary>
/// Python quotes the argument of a base that names the class being declared, because a base list
/// is an expression Python evaluates before the name exists.
/// </summary>
/// <remarks>
/// The self-type idiom, which is how an interface hands a method the implementing type — every one
/// of the 212 generated quantities in <c>ktsu.Semantics</c> is declared this way. Unquoted, the
/// module raises <c>NameError</c> on import rather than misbehaving later, so this is the
/// difference between a file that can be loaded and one that cannot.
/// </remarks>
[TestMethod]
public void Python_QuotesABaseArgumentNamingTheClassBeingDeclared()
{
string code = Generate(new PythonGenerator(), SelfTyped());

Assert.Contains("class Length(IVector0[\"Length[T]\", T]):", code);
}

/// <summary>
/// The same for a pair that refer to each other, which needs no generics of its own.
/// </summary>
[TestMethod]
public void Python_QuotesABaseArgumentThatIsTheClassItself()
{
ClassDeclaration node = new("Node");
node.Interfaces.Add(TypeReference.Parse("Visitor<Node>"));

string code = Generate(new PythonGenerator(), node);

Assert.Contains("class Node(Visitor[\"Node\"]):", code);
}

/// <summary>
/// An argument holding the class further down is quoted whole, which is the one place the quote
/// can go: a second pair inside the first would end the string rather than nest.
/// </summary>
[TestMethod]
public void Python_QuotesTheWholeArgumentWhenTheClassIsNestedInIt()
{
ClassDeclaration length = new("Length");
length.Interfaces.Add(TypeReference.Parse("IVector0<Wrapper<Length<T>>, T>"));

string code = Generate(new PythonGenerator(), length);

Assert.Contains("class Length(IVector0[\"Wrapper[Length[T]]\", T]):", code);
}

/// <summary>
/// An argument naming anything else is left alone: a forward reference is what a name that does
/// not exist yet needs, and every other name is one Python can already resolve.
/// </summary>
[TestMethod]
public void Python_LeavesABaseArgumentNamingSomethingElseUnquoted()
{
ClassDeclaration button = new("Button");
button.Interfaces.Add(TypeReference.Parse("Handler<Event>"));

string code = Generate(new PythonGenerator(), button);

Assert.Contains("class Button(Handler[Event]):", code);
Assert.DoesNotContain("\"", code, "Python quoted a base that names nothing being declared.");
}

/// <summary>
/// The other targets write the same declaration verbatim, because the declaration is not what is
/// wrong with it: Python is the one target that reads a base list eagerly.
/// </summary>
[TestMethod]
public void OtherTargets_WriteASelfTypedInterfaceVerbatim()
{
Assert.Contains(
"class Length : IVector0<Length<T>, T>", Generate(new CSharpGenerator(), SelfTyped()));
Assert.Contains(
"class Length : public IVector0<Length<T>, T>", Generate(new CppGenerator(), SelfTyped()));
}

/// <summary>
/// JavaScript extends one thing and has no interfaces, so it writes down what it cannot say.
/// </summary>
Expand Down
98 changes: 86 additions & 12 deletions Coder/Languages/PythonGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,11 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod
// Python inherits from as many things as it is given and has no separate notion of an
// interface, so the base and the interfaces are one list of bases -- which is what the
// abstract base classes in the standard library already are.
string? declaring = classDecl.Name;
string[] bases =
[
.. classDecl.BaseType is TypeReference baseType ? (string[])[PythonTypeFromGenericType(baseType)] : [],
.. classDecl.Interfaces.Select(PythonTypeFromGenericType),
.. classDecl.BaseType is TypeReference baseType ? (string[])[PythonBaseFromGenericType(baseType, declaring)] : [],
.. classDecl.Interfaces.Select(implemented => PythonBaseFromGenericType(implemented, declaring)),
];

if (bases.Length > 0)
Expand Down Expand Up @@ -678,16 +679,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code,
/// </remarks>
private static string PythonTypeFromGenericType(TypeReference type)
{
string name = type.Name.ToLowerInvariant() switch
{
"int" => "int",
"string" => "str",
"bool" => "bool",
"float" => "float",
"double" => "float",
"void" => "None",
_ => type.Name
};
string name = PythonTypeName(type);

string spelled = type.TypeArguments.Count == 0
? name
Expand All @@ -698,6 +690,88 @@ private static string PythonTypeFromGenericType(TypeReference type)
return type.IsArray ? $"list[{spelled}]" : spelled;
}

/// <summary>
/// Maps a type's name to the Python spelling of it.
/// </summary>
/// <param name="type">The type whose name to map.</param>
/// <returns>The name Python knows it by.</returns>
private static string PythonTypeName(TypeReference type) => type.Name.ToLowerInvariant() switch
{
"int" => "int",
"string" => "str",
"bool" => "bool",
"float" => "float",
"double" => "float",
"void" => "None",
_ => type.Name
};

/// <summary>
/// Spells a base of a class, quoting any argument that names the class being declared.
/// </summary>
/// <param name="type">The base to spell.</param>
/// <param name="declaring">The name of the class the base list belongs to.</param>
/// <returns>The Python source for it.</returns>
/// <remarks>
/// A base list is an expression and Python evaluates it as the <c>class</c> statement is run, so a
/// base naming the class being declared is a name that does not exist yet: the self-type idiom —
/// <c>IVector0&lt;TSelf, T&gt;</c>, the interface that hands a method the implementing type — makes
/// the module raise <c>NameError</c> on import rather than misbehave later. Nothing about the
/// declaration is wrong; Python is the one target that reads a base eagerly.
/// <para>
/// A quoted argument is the language's own answer, and the one <c>typing</c> consumers write for
/// exactly this case: <c>typing</c> takes a string as a forward reference and resolves it when
/// something asks, by which time the class exists. <c>from __future__ import annotations</c> is
/// the other half of the idea and does not reach here — it defers *annotations*, and a base is an
/// expression.
/// </para>
/// <para>
/// Only the arguments are quoted, never the base itself. A class may inherit from a subscripted
/// generic holding a forward reference and may not inherit from a string, and quoting is at the
/// outermost argument alone for the same reason: one quoted argument carries every name inside it,
/// and a second pair of quotes within the first would end the string rather than nest.
/// </para>
/// </remarks>
private static string PythonBaseFromGenericType(TypeReference type, string? declaring)
{
if (declaring is null || type.TypeArguments.Count == 0 || !NamesType(type.TypeArguments, declaring))
{
return PythonTypeFromGenericType(type);
}

IEnumerable<string> arguments = type.TypeArguments.Select(argument =>
NamesType(argument, declaring)
? $"\"{PythonTypeFromGenericType(argument)}\""
: PythonTypeFromGenericType(argument));

string spelled = $"{PythonTypeName(type)}[{string.Join(", ", arguments)}]";

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

/// <summary>
/// Asks whether a type names another anywhere in it.
/// </summary>
/// <param name="type">The type to look through.</param>
/// <param name="name">The name being looked for.</param>
/// <returns>True when the name is the type's own or any of its arguments'.</returns>
/// <remarks>
/// Through the arguments rather than at the top alone, because a base is as readily written over a
/// type holding the class — <c>IVector0&lt;Wrapper&lt;Length&lt;T&gt;&gt;, T&gt;</c> — as over the
/// class itself, and Python evaluates all of it at once either way.
/// </remarks>
private static bool NamesType(TypeReference type, string name) =>
string.Equals(type.Name, name, StringComparison.Ordinal) || NamesType(type.TypeArguments, name);

/// <summary>
/// Asks whether any of several types names another anywhere in it.
/// </summary>
/// <param name="types">The types to look through.</param>
/// <param name="name">The name being looked for.</param>
/// <returns>True when any of them names it.</returns>
private static bool NamesType(IEnumerable<TypeReference> types, string name) =>
types.Any(type => NamesType(type, name));

/// <inheritdoc/>
/// <remarks>
/// A constant is emitted as an ordinary assignment: Python has no constant declaration, and the
Expand Down