Extension: C# as a third front-end language (Roslyn) — architectural fit analysis
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 8/100
調査の方向性
This is a design analysis, not a code task: start with docs/kotlin-parity-study-2026-09-26.md and ARCHITECTURE.md, then read DetailedSources.java (around line 119) and LinkNatureImpl.java (lines 15-56). The first concrete step is the proposed one-day Java spike that tests whether a struct copy maps to the shares-fields (≈) link. Done means the spike's verdicts are reported on this issue and the Predefined refactor is scoped.
索引モデルが issue の本文から書いたものです。
説明
Question
Can C# be added as a third front-end language next to Java and Kotlin, with parity as the eventual aim, reading the semantically resolved tree from Roslyn? Can that happen within the current maddi / maddi-mod architecture, or would it need a redesign?
Verdict
It fits; no redesign is needed. C# becomes a third front end in maddi. maddi-mod would need extensions in places already shaped to take them. The one structural refactor is in the CST's type layer, which assumes Java's primitives and root types (item 1 under "Where it bends"). It is a lot of work but carries no design risk, and it should come before any C# lowering.
This is a desk analysis, based on docs/kotlin-parity-study-2026-09-26.md, ARCHITECTURE.md and the CST and analysis sources. Nothing has been run. The weakest claim is that a struct copy maps to the existing "shares fields" (≈) link; the first step of the plan is a spike that tests it.
Why it fits
-
One shared tree has already been tested with a second language. Kotlin is converted into the existing CST: properties become a getter and setter,
Int?becomes a boxed type,suspendbecomes aContinuationparameter. The Kotlin-specific information the CST needed was added as side data rather than new node types:DetailedSourcesmarkers for?:,?.,a[i]and!!, import aliases, andNullableState. That side data was meant to serve several languages;DetailedSources.java:150-155names C#, Groovy and TypeScript. C#??,?., indexers andispatterns would reuse the same markers. -
The analysis code is language-neutral. prepwork, link and the analyzer contain no Kotlin names. The JDK names they contain sit in 8 files, about 15 string constants:
List/Set/Map.of|copyOfas defensive copies, thejava.util.streamprimitive streams,Objects.isNull/nonNull, andAtomicBoolean. These become hooks each language supplies, a small, contained change. -
Value types fit the existing link vocabulary (
LinkNatureImpl.java:15-56):- Assigning a struct variable copies the value. That is "shares fields" (≈), the link a shallow copy produces, not "identical to" (≡).
- A
readonly struct, or one with only value fields, needs no links at all, which is howStringis already treated.
The front end chooses the link nature from the type; the link algebra itself does not change.
-
ref,outandincan be lowered the way compilers lower them. kotlinc turns a captured mutable local into a one-element holder (Ref.ObjectRef). Doing the same forref/outturns variable aliasing into "an object with one field", which the link engine already handles. It loses some precision but needs no structural change.reflocals/returns,Span<T>,unsafe,dynamicandgotowould be refused with a named placeholder, as the Kotlin front end refuses its unsupported shapes. -
Nullability lines up directly.
#nullable enable,T?and the[NotNullWhen]/[MaybeNull]/[MemberNotNull]attributes follow the same model as JSpecify's@NullMarkedand its annotations.NullabilityPass.Policyalready chooses behaviour per language (Kotlin is one value of it), so C# would be another. Roslyn also computes a nullability state for every expression, which could check the pass the way the guava oracle checks it for Java. -
Library types come from one source. Roslyn provides the symbols of referenced assemblies, so the Kotlin front end's "two models" problem (K2 symbols vs. class files, parity study §2.2) does not arise.
Where it bends
- The CST is Java-shaped at the type level.
DetailedSources.java:119says outright that the CST "models Java".TypeInfohasisJavaLangObject,isJavaLangString,isBoxedLongand similar, plus a fixed set of Java primitives, andPredefinedis built around them. Kotlin avoided this by mapping its types onto the JVM's (Any→java.lang.Object). C# cannot:System.Stringis notjava.lang.String, and their contracts differ.uint,ulong,decimalandnintalso don't exist in Java.
Fix:Predefinedsupplies the root, string, primitive and numeric types for each language, and the predicates ask it instead of comparing JVM names. This touches many call sites but carries no design risk, and Kotlin benefits too. - Type identity is a fully qualified name. C# allows
List<T>andList<T,U>side by side, so the generic arity has to be part of the name (List`1). That convention should be decided once in the CST API, not left to the front end. - Contract authoring is tied to the JVM. Contracts are written as Java shadow classes and resolved against bytecode. C# needs a counterpart written in C# and resolved through Roslyn; the archive format and how it is loaded stay the same. The BCL archive is a large job in its own right. An uncontracted method defaults to "modifies"; the JDK archive has 4,877 annotated members, while the Kotlin archive has 31 and is already its largest verdict gap (parity study §5.2). LINQ adds lazy evaluation: a lambda passed to it runs when the result is iterated, not when the call is made.
- The build. The Roslyn exporter is a .NET project and doesn't belong in the Gradle build. Under the planned three-repo split (
ARCHITECTURE.md:4), it would go in its own small repo or adotnet/subproject. It would publish a versioned serialized schema read by amaddi-csharp-*module pair inmaddi, following the producer/driver pattern the other front ends use. Nothing inmaddi-modwould depend on .NET.
C# features against the current CST
| C# feature | Mapping |
|---|---|
Properties, indexers, events, operators, implicit conversions, extension methods, using, foreach, lock, local functions, records, pattern matching, tuples |
Lower to calls. Roslyn's IOperation tree already resolves these to method symbols. Front end only. |
| Delegates and lambdas | Become functional interfaces, but they must count as standard ones. Otherwise every delegate parameter defaults to "modified", the same trap as Kotlin's FunctionN. Front end, plus a small change to one analyzer predicate. |
async/await, yield return |
Start as opaque calls, as Kotlin's suspend does. Lower to the compiler's state machine later if precision requires it. |
Reified generics, typeof(T), struct/unmanaged constraints |
Generic arity goes into the name (item 2); constraints are carried on the type parameter. |
| Structs | Copies are ≈ links; readonly structs have no links. Needs the spike below. |
ref/out/in parameters |
Lowered to holder objects. |
ref locals/returns, Span<T>, goto, unsafe, dynamic, stackalloc |
Refused with a named placeholder. |
Effort
For comparison, the Kotlin front end is about 12k lines (maddi-kotlin-k2 plus its drivers), a sixth of the Java one. A rough estimate for C#:
- an exporter of 4–6k lines of C#;
- a CST builder of 10–15k lines, mostly lowering code;
- the
Predefinedrefactor; - the BCL contract archive, which grows corpus by corpus.
Overall, roughly 3–5× what Kotlin has cost so far to reach Kotlin's current level.
Plan
- Spike, about 1 day (blocks the rest): confirm that ≈ is the right link for a struct copy. Write it in Java: a mutable "struct" class whose assignments are replaced by shallow copies, then copied, modified and passed around. Check the verdicts.
- Refactor
Predefinedso the language supplies the root, string, primitive and numeric types (item 1). Fix the generic-arity naming convention (item 2). - Spike, 2–3 weeks (go/no-go): an exporter covering classes, methods, statements and calls, a CST builder, and one small corpus with no value types. Run prepwork, link and the analyzer on it. Write a probe of shapes that convert without error but mean something else, like the Kotlin parity study §2.3, so silent-wrong lowerings are counted from day one.
- Front-end coverage, with a placeholder census and two-sided ratchets per corpus. Grow the BCL archive and its C# authoring tool (item 3) alongside.
- Nullability, with Roslyn's nullable-reference-type analysis as the oracle.
- A parity study modelled on
docs/kotlin-parity-study-2026-09-26.md.
Out of scope for now
- A C# printer and rewiring. They are needed only if maddi will rewrite or generate C#; the printer would be about 1k lines, like
cst-print-kotlin, given lowerings that can be reversed. - MSBuild/NuGet restore integration.
- IDE clients.
- 主要言語
- Java
- スター
- 1
- フォーク
- 1
- 平均マージ
- 2時間 22分
- マージ済み PR(30日)
- 4
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
CodeLaser/maddi のほかの issue
-
build/ci good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
-
C# printer: an inner class of a generic class needs the outer type's arguments (Outer<A>.Inner)オープンfront-end:csharp
難易度 4/5 3〜5日 初心者へのやさしさ 30/100
-
難易度 5/5 1週間以上 初心者へのやさしさ 10/100
-
Java → C# translation: an idiomatic C# printer driven by the modification and nullability analysesオープンenhancement extension front-end:csharp
難易度 5/5 1週間以上 初心者へのやさしさ 8/100
-
難易度 5/5 1週間以上 初心者へのやさしさ 28/100
CodeLaser/maddi の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
scylladb/scylla-cdc-source-connector#350 ·
メンテナーはふだん 1 日以内に返信
-
agent:working documentation enhancement go:build lane:needs-decision
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
Sanne/incus-spawn#1435 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
area:shuffle bug priority:high requires-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
apache/datafusion-comet#7062 ·
メンテナーはふだん 1 日以内に返信
-
wait for review
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
enhancement good first issue up-for-grabs
難易度 2/5 1〜3時間 初心者へのやさしさ 80/100
AnikethTS/ChangeRadar#22 ·
メンテナーはふだん 1 日以内に返信