You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
C#: member operators lose their implicit operand, so binary + silently becomes unary + and unary/conversion operators fail with CS1019 #126
Operators in the AST are member-style: the receiver is implicit, as in C++ (Money operator+(Money rhs)), Rust (impl Add for Point { fn add(self, rhs) }) and Go. CSharpGenerator writes them as C# operators, which are always static and take every operand explicitly. It adds static (Coder/Languages/CSharpGenerator.cs:734-738) but writes the AST's parameters unchanged, so the implicit operand is never added. As a result:
A binary operator with one parameter compiles, but as a unary operator. a + b on the type then fails at every call site, and nothing in the generated file shows why.
A unary operator with no parameters (CompiledExemplar.Negate) and a conversion operator with no parameters fail with CS1019.
With IsReadOnly = true (as in CompiledExemplar.Plus, Negate and ToDouble), the output is static readonly, which fails with CS0106 (:749-752).
publicstructMoney{publicintcents;publicstaticMoneyoperator+(Moneyrhs)// compiles, but as UNARY plus{returnrhs;}publicstaticexplicitoperatorint()// error CS1019{return0;}}
The C++ output for the same AST is Money operator+(Money rhs) (binary) and explicit operator int(). The exemplar's Plus and Negate come out as public static readonly Point operator +(Point rhs), which fails with CS0106. All errors were confirmed with dotnet build on the generated file (net10.0).
Suggested fix / acceptance criteria
For FunctionKind.Operator and ConversionOperator inside a type, prepend a parameter of the enclosing type for the implicit operand, named self as in the C lowering, so bodies that use self.x read the same. A binary operator then has two parameters, a unary operator one, and a conversion takes its source.
Never write readonly on a static member.
Add a CSharpGeneratedSourceCompilesTests that compiles the generated C#, for example via Microsoft.CodeAnalysis.CSharp, like the existing Cpp/C/Rust/GoGeneratedSourceCompilesTests. Cover CompiledExemplar.Plus(), Negate() and ToDouble(...) on Point(). C# is currently the only statically typed target whose output no test compiles.
Priority: High. A binary operator compiles as a unary +, which silently changes what the generated code means, and every call site then fails with nothing in the generated file to explain why. Unary and conversion operators don't compile at all (CS1019). C# is also the only statically typed target whose output no test compiles, which is how this went unnoticed.
Area:CSharpGenerator: operator lowering and WriteFunctionModifiers
Activity
matt-edmondson commentedon Sep 29, 2026
Triage
+, which silently changes what the generated code means, and every call site then fails with nothing in the generated file to explain why. Unary and conversion operators don't compile at all (CS1019). C# is also the only statically typed target whose output no test compiles, which is how this went unnoticed.CSharpGenerator: operator lowering andWriteFunctionModifiersreadonlyon class/interface members (CS0106),public ~T()(CS0106), abstract member in a non-abstract class (CS0513) #127 has the same root,WriteFunctionModifierswriting modifiers without checking the enclosing type, and shares thestatic readonly→ CS0106 symptom. Both issues ask for a C# compile test. Doing C#: member operators lose their implicit operand, so binary+silently becomes unary+and unary/conversion operators fail with CS1019 #126 and C#: modifiers are emitted where C# forbids them:readonlyon class/interface members (CS0106),public ~T()(CS0106), abstract member in a non-abstract class (CS0513) #127 as one change, with that test landed first, is the cheapest path. No EntryPoint placement builds in every target: C# rejects it at file/namespace scope, C++/Rust losemaininside a namespace, and Rust/Go drop it silently inside a class #128 is also C#-output correctness, but it concerns placement rather than modifiers.Generated by Claude Code