Skip to content

Add a C language generator - #54

Merged
matt-edmondson merged 4 commits into
mainfrom
claude/practical-mendel-gg7bjv
Sep 12, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
claude/practical-mendel-gg7bjv

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Adds CGenerator, a fifth target alongside C#, C++, Python and JavaScript.

C is the first target with no classes, no namespaces, no overloading and no generics, so the interesting part is what it writes in their place. Each substitution is the one C code written by hand uses, rather than a comment apologising for the language:

The AST says C writes
ClassDeclaration typedef struct Point { … } Point;, keeping the tag so a struct can point at its own kind
a member function a free void Point_translate(Point* self, int dx); IsReadOnly becomes const Point* self, and a static member takes no instance
a constructor a function returning the value it built, from the initialiser list written as the designated initialiser C invented
an interface a struct of function pointers, each taking the instance as void* — what every C library that dispatches dynamically does
BaseType the first member, which is what makes the two layout-compatible
an enumeration typedef enum Colour { Colour_Red, … } Colour; — a C enumeration is unscoped, so two with a None each would be one name declared twice
a namespace a comment over flat members; folding the name into the declarations would rename them without renaming the references to them elsewhere in the AST
an operator the word for what it does (Vec_add), since C cannot declare one
a constant static const at file scope — a file-scope const in C has external linkage, so a header declaring one and included twice would not link

The dialect is C99 plus C11's _Static_assert. Nothing needs C23, which is why a pure function gets no [[nodiscard]] and an enumeration no fixed underlying type — both would restrict the output to a dialect most C is still not compiled as, and neither changes what the program means.

What C genuinely cannot say is written as a note rather than dropped: a struct member's initial value, a parameter's default, a deleted declaration (writing its prototype would make legal exactly what the declaration exists to forbid).

One distinction turned out to be load-bearing and is worth flagging: a braced list is written as an initialiser ({ .x = 1 }) where a declaration has already said its type, and as a compound literal ((Point){ .x = 1 }) only where a value stands on its own. Only the first is a constant expression, which is what an object with static storage duration has to be initialised by — so a generated constant table written the other way does not compile at file scope.

Testing

  • CGeneratorTests — 35 tests pinning the spellings and the decisions above.
  • CGeneratedSourceCompilesTests — generates a header holding one of everything and compiles it with a real C compiler (cc/gcc/clang), from a driver that includes it twice and uses every declaration it makes. C's rules about linkage, what an empty parameter list declares, and what may initialise a static-storage object are not visible in the text, so a spelling can be pinned and still be wrong. Inconclusive where no compiler is on the path, which is the honest result there.
  • The registration, line-ending, syntax-highlighting and editor-wiring tests that enumerate generators now include C; the highlighter already knows c.
  • Full suite: 534 passed, 0 failed.

README, CLAUDE.md and docs/design.md are updated, including the claims that were true only while C++ was the one target with a compile-time assertion.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7


Generated by Claude Code

C is the first target with no classes, namespaces, overloading or generics,
so CGenerator writes what C uses in their place rather than dropping them:

- a type is a `typedef struct` keeping its tag
- a member function is a free `Type_name(Type* self, ...)`, and a `const`
  member function takes a pointer to const
- a constructor returns the value it built from a designated initialiser
- an interface is a struct of function pointers taking an untyped receiver
- a base type is the first member, which is what makes the two
  layout-compatible
- an enumeration's members are qualified by its name, since a C enumeration
  is unscoped
- a namespace is a comment over flat members: folding the name into the
  declarations would rename them without renaming the references to them

The dialect is C99 plus C11's `_Static_assert`, so a pure function gets no
`[[nodiscard]]` and an enumeration no fixed underlying type. What C cannot
say - a member's initial value, a default argument, a deleted declaration -
is written as a note rather than dropped.

CGeneratedSourceCompilesTests compiles a generated header with a real C
compiler, including it twice from one translation unit, 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. It is inconclusive
where no compiler is on the path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
SonarCloud's quality gate failed the PR on duplication: 199 of the new
lines in CGenerator were duplicates of CppGenerator, in three blocks.

CFamilyGenerator now sits between the two and StandardLanguageGenerator
and owns what they share, all of which is about C rather than about the
AST: `#pragma once` and `#include`, the braced list and its designated
initialisers, the declarator that puts an array's brackets after the name,
the member-grouping rule, and the dispatch from a bare function
declaration into the emitter that knows which type it belongs to. A
generator supplies `SpellType` and, where it differs, how a value inside a
list is written - which is the one thing C needs, since only there may it
leave the type out.

The type mappings stay with each generator: `str` is a `std::string` in
one language and a `const char*` in the other, and the whole of what a
mapping is is the spelling.

C's operator names are now derived from the AST's own operator vocabulary
rather than listed beside it, so an operator added to `BinaryOperator` is
named without anybody remembering to, and the names cannot drift from the
symbols `OperatorSymbols` spells.

No output changes: all 534 tests pass, including the ones that pin C++'s
generated source exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
Comment thread Coder/Languages/CGenerator.cs Fixed
Comment thread Coder/Languages/CGenerator.cs Fixed
claude and others added 2 commits September 11, 2026 22:45
The review bot's finding: both loops in BuildOperatorNames filtered inside
the body, so the sequence being iterated was not the sequence being used.

`HasSymbol` is what the filter now reads as, and it is also what makes
`GetSymbol` safe to call on what survives it - an operator the AST has no
spelling for is left out rather than throwing before anything has run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7
@matt-edmondson
matt-edmondson merged commit 63c2fc4 into main Sep 12, 2026
6 checks passed
@matt-edmondson
matt-edmondson deleted the claude/practical-mendel-gg7bjv branch September 12, 2026 03:54
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants