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
31 changes: 24 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dotnet run --project Coder.Editor
## Project Structure

`ktsu.Coder` represents code as a language-agnostic AST, round-trips it through YAML, and generates
source in five target languages. The solution uses:
source in six target languages. The solution uses:

- **ktsu.Sdk** — custom SDK providing shared build configuration
- **MSTest.Sdk** — test project SDK with Microsoft Testing Platform
Expand Down Expand Up @@ -79,23 +79,27 @@ source in five target languages. The solution uses:
C++ is the one language here that puts them on the declarator rather than the type.
`IsConstant` is the intent rather than the keyword: C++ writes `inline constexpr` at namespace
scope and `static constexpr` inside a type, C# writes `static readonly`, C writes `static const`
at file scope and a note inside a struct, having no static data member at all, and a language with no
spelling for it omits it the way it omits an indirection.
at file scope and a note inside a struct, having no static data member at all, Rust picks between
a `const` and a `static` with it and lets it decide whether a local is `let` or `let mut`, and a
language with no spelling for it omits it the way it omits an indirection.
- `Coder/Ast/ClassDeclaration.cs`'s `SpecialisationArguments` — what makes a declaration be *for* a
type rather than *of* one. `template<> struct Describe<RigidBody>` is how C++ attaches a fact to a
type without touching the type, which is what a generated reflection table needs: the alternative
is naming, and a `DescribeRigidBody` every consumer has to spell for itself is the thing a lookup
by type exists to avoid. Only C++ has it and the other three write a comment, the same as
`CompileTimeAssertion`; the arguments are `TypeReference` rather than text, though, because a
by type exists to avoid. C++ has it, and Rust answers it exactly — `impl Describe for RigidBody`
attaches facts to a type without touching the type, which is the whole of what the specialisation
is for; the rest write a comment, the same as `CompileTimeAssertion`. The arguments are
`TypeReference` rather than text, though, because a
specialisation argument is a type and the comma in `Result<Handle, Error>` belongs to one of them
rather than separating two.
- `Coder/Ast/CompileTimeAssertion.cs` — what a generated type promises that the type itself cannot
say. Its `Condition` is text for the same reason `SourceFile.Imports` are: a compile-time predicate
is language-specific in a way most of the AST is not, and there is no shared idea underneath
`std::is_trivially_copyable_v<T>` to model. Only C++ and C have one — `static_assert` and
`_Static_assert`, the second of which requires a message, so an assertion with none is given its
own condition; the others write a comment, because a file that quietly loses a guarantee looks
like one that still makes it.
own condition — and Rust, whose `const _: () = assert!(…)` needs no macro crate because a constant
nobody names still has to be evaluated for the program to build; the others write a comment,
because a file that quietly loses a guarantee looks like one that still makes it.
- `Coder/Languages/LanguageGeneratorBase.cs` — the emitters every generator shares.
- `Coder/Languages/StandardLanguageGenerator.cs` — owns the node dispatch, so a derived
generator supplies only the syntax its language does not share. `CSharpGenerator` deliberately
Expand All @@ -116,6 +120,19 @@ source in five target languages. The solution uses:
underlying type. `Coder.Test/Languages/CGeneratedSourceCompilesTests.cs` compiles what it writes,
because C's rules about linkage, empty parameter lists and what may initialise an object with
static storage duration are not visible in the text.
- `Coder/Languages/RustGenerator.cs` — the target with the most to map onto, and so the one where the
interesting question is which feature each part of a declaration became rather than what to write
in place of it. Data goes in a `struct` and behaviour in an `impl` block; an interface is a `trait`
and a base type on one is a supertrait; a destructor is `impl Drop`, an operator is its `std::ops`
trait, a conversion is `impl From`, and a specialisation is `impl Trait for Type` — which is the
one place a target answers C++'s explicit specialisation exactly. What is left over is inheritance,
which Rust does not have, and the operators it supplies from another one and will not let a type
define by itself. `Coder.Editor/RustSyntax.cs` registers the highlighter definition, because the
highlighter ships fifteen languages and Rust is not one of them.
- `Coder.Test/Languages/CompiledExemplar.cs` — one AST, compiled by two real compilers. The C and
Rust generators are each checked by compiling what they write, and they are checked against the
same declarations, which says more than two parallel fixtures could: the claim being made is that
the same AST comes out as valid source in each target.
- `Coder.Graph/AstSchema.cs` — the uniform view of the AST's parent/child structure, hand-written
rather than reflective. Adding a node type means adding it here.
- `Coder.Graph/AstFields.cs` — a node's editable properties as named fields of a kind, which is what
Expand Down
1 change: 1 addition & 0 deletions Coder.Editor/Coder.Editor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="ktsu.ImGui.App" />
<!-- The preview pane draws the generated source highlighted rather than as flat text. -->
<PackageReference Include="ktsu.ImGui.SyntaxHighlighting" />
<PackageReference Include="ktsu.SyntaxHighlighting" />
<!-- The panes are divider containers, so the user sizes them rather than the application. -->
<PackageReference Include="ktsu.ImGui.Widgets" />
<!-- Remembering whether the window was maximized means naming WindowState, which lives here
Expand Down
10 changes: 10 additions & 0 deletions Coder.Editor/CoderEditorApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
{
ClassDeclaration declaration = new("Counter");

declaration.Members.Add(new VariableDeclaration("count", "int", new LiteralExpression<int>(0)));

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.

Check warning on line 144 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'count' 5 times.
declaration.Members.Add(new VariableDeclaration("step", "int", new LiteralExpression<int>(1)));

FunctionDeclaration add = new("Add") { ReturnType = "int" };
Expand Down Expand Up @@ -323,7 +323,7 @@
/// Draws the application's File menu.
/// </summary>
/// <remarks>Called from inside the application's main menu bar, so it opens no bar of its own.</remarks>
public void DrawMenu()

Check warning on line 326 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 326 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.

Check warning on line 326 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed.
{
if (ImGui.BeginMenu("File"))
{
Expand Down Expand Up @@ -360,7 +360,7 @@

if (Settings.RecentFiles.Count > 0 && ImGui.BeginMenu("Recent"))
{
foreach (string recent in Settings.RecentFiles.ToArray())

Check warning on line 363 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 363 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 363 in Coder.Editor/CoderEditorApp.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (ImGui.MenuItem(recent))
{
Expand Down Expand Up @@ -418,6 +418,16 @@
/// </remarks>
private static readonly SyntaxHighlightConfig CodeStyle = new() { ShowLineNumbers = true };

/// <summary>
/// Teaches the highlighter the one language it does not already know.
/// </summary>
/// <remarks>
/// A static constructor rather than a call from somewhere: the registry is process-global and the
/// preview reaches it from a draw call, so the registration has to have happened before any
/// instance of this class draws anything, whichever one draws first.
/// </remarks>
static CoderEditorApp() => RustSyntax.Register();

/// <summary>
/// Draws the generated source, highlighted for the language it was generated in.
/// </summary>
Expand Down
97 changes: 97 additions & 0 deletions Coder.Editor/RustSyntax.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Editor;

using ktsu.SyntaxHighlighting;

/// <summary>
/// Teaches the syntax highlighter to read Rust.
/// </summary>
/// <remarks>
/// The highlighter ships definitions for fifteen languages and Rust is not one of them, so the
/// preview pane would draw generated Rust as plain text — the one failure mode
/// <c>ImGuiSyntaxHighlighting</c> has, and a silent one. Definitions are plain data and the registry
/// takes one from an application, which is what this is.
/// <para>
/// It lives in the editor rather than in the library for the reason the library has no UI
/// dependency at all: what a language looks like on a screen is the editor's business, and
/// <c>RustGenerator</c> would not otherwise know that a highlighter exists.
/// </para>
/// <para>
/// The keyword list is the language's, not the generator's. A highlighter reads whatever is in the
/// pane — a file somebody opened, or output from a generator that has learned a new spelling since
/// — so listing only what this generator emits today would make the pane wrong tomorrow.
/// </para>
/// </remarks>
internal static class RustSyntax
{
/// <summary>
/// The language name the generator reports and the highlighter is asked for.
/// </summary>
private const string LanguageName = "rust";

/// <summary>
/// Registers the definition, if it is not registered already.
/// </summary>
/// <remarks>
/// Idempotent, and deliberately: the registry is process-global, the editor builds more than one
/// application object over a test run, and registering twice would replace a definition with an
/// identical one for no reason.
/// </remarks>
public static void Register()
{
if (LanguageRegistry.TryGet(LanguageName, out _))
{
return;
}

LanguageRegistry.Register(Definition);
}

/// <summary>
/// Gets what the tokenizer needs in order to read Rust.
/// </summary>
private static LanguageDefinition Definition => new()
{
Name = LanguageName,
Aliases = ["rs"],
CaseSensitive = true,

// `///` and `//!` are documentation and are drawn as such; `//` is an ordinary comment. The
// documentation forms come first, because the first rule that matches wins and every one of
// them starts with the ordinary one.
LineComments =
[
new LineCommentRule { Prefix = "///", Kind = TokenKind.DocComment },
new LineCommentRule { Prefix = "//!", Kind = TokenKind.DocComment },
new LineCommentRule { Prefix = "//" },
],
BlockComments = [new BlockCommentRule { Open = "/*", Close = "*/" }],
Strings =
[
new StringRule { Open = "\"", Close = "\"" },
new StringRule { Open = "'", Close = "'" },
],

Keywords =
[
"as", "async", "await", "const", "crate", "dyn", "enum", "extern", "fn", "impl", "in",
"let", "mod", "move", "mut", "pub", "ref", "self", "Self", "static", "struct", "super",
"trait", "type", "union", "unsafe", "use", "where",
],
ControlKeywords = ["break", "continue", "else", "for", "if", "loop", "match", "return", "while"],
Types =
[
"bool", "char", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "str",
"u8", "u16", "u32", "u64", "u128", "usize", "String", "Vec", "Box", "Option", "Result",
"HashMap", "HashSet",
],
Constants = ["true", "false", "None", "Some", "Ok", "Err"],

HighlightFunctionCalls = true,
IdentifierCharacters = "_",
IdentifierStartCharacters = "_",
OperatorCharacters = "+-*/%=<>!&|^~?:",
PunctuationCharacters = "(){}[];,.",
};
}
2 changes: 1 addition & 1 deletion Coder.Test/Editor/CoderEditorAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private static DocumentStore NewStore() =>
private string PathIn(string name) => Path.Combine(root, name + DocumentStore.Extension);

private static CoderEditorApp NewApp(DocumentStore store, EditorSettings? settings = null) =>
new(store, [new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new CGenerator(), new JavaScriptGenerator()],
new(store, [new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new CGenerator(), new RustGenerator(), new JavaScriptGenerator()],
settings ?? new EditorSettings());

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Coder.Test/Editor/EditorWiringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public sealed class EditorWiringTests
{
private const string ConfigHomeVariable = "XDG_CONFIG_HOME";

private static readonly string[] ExpectedLanguageIds = ["python", "csharp", "javascript", "cpp", "c"];
private static readonly string[] ExpectedLanguageIds = ["python", "csharp", "javascript", "cpp", "c", "rust"];
private static readonly string[] ExpectedRecentFiles = ["/work/second.coder.yaml", "/work/first.coder.yaml"];

private string root = string.Empty;
Expand Down
16 changes: 15 additions & 1 deletion Coder.Test/Editor/GeneratedCodeHighlightingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ktsu.Coder.Test.Editor;

using ktsu.Coder.Ast;
using ktsu.Coder.Editor;
using ktsu.Coder.Languages;
using ktsu.ImGui.SyntaxHighlighting;
using ktsu.SyntaxHighlighting;
Expand Down Expand Up @@ -35,8 +36,21 @@ private static FunctionDeclaration SampleFunction()
return function;
}

/// <summary>
/// Teaches the highlighter what the editor teaches it.
/// </summary>
/// <remarks>
/// The highlighter ships fifteen languages and Rust is not one of them, so the editor registers a
/// definition for it at startup. This is that same registration: without it the assertion below
/// would be checking a language nothing had told the highlighter about, and with it the
/// assertion checks the definition as well as the id.
/// </remarks>
/// <param name="context">The test context, which this does not read.</param>
[ClassInitialize]
public static void RegisterEditorLanguages(TestContext context) => RustSyntax.Register();

private static ILanguageGenerator[] Generators() =>
[new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new CGenerator(), new JavaScriptGenerator()];
[new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new CGenerator(), new RustGenerator(), new JavaScriptGenerator()];

/// <summary>
/// Tests that every generator's language id is one the highlighter recognises, by generating real
Expand Down
Loading