From df7addb33f224cb20b0049f0b70cd3cc55d5e381 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 18:32:18 +0000 Subject: [PATCH] Number Python and JavaScript enum members on from the previous value [patch] A member with no value of its own took its position, so after an explicit value the two languages disagreed with C, C++, Rust and Go and could repeat an earlier value, which Python turns into a silent alias. Fixes #79. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012B4b6sZaZ7tFJzNvax1B89 --- Coder.Test/Ast/TypeDeclarationTests.cs | 49 +++++++++++++++++++++++- Coder/Languages/JavaScriptGenerator.cs | 5 +-- Coder/Languages/LanguageGeneratorBase.cs | 40 +++++++++++++++++++ Coder/Languages/PythonGenerator.cs | 9 ++--- 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/Coder.Test/Ast/TypeDeclarationTests.cs b/Coder.Test/Ast/TypeDeclarationTests.cs index 8157ec2..2dc738a 100644 --- a/Coder.Test/Ast/TypeDeclarationTests.cs +++ b/Coder.Test/Ast/TypeDeclarationTests.cs @@ -111,13 +111,58 @@ public void Enum_IsSpelledByEachLanguage() StringComparison.Ordinal); Assert.Contains("Dynamic = 7,", new CppGenerator().Generate(kind), StringComparison.Ordinal); - // Python and JavaScript have no enumeration, so a member with no value of its own is numbered - // from its position rather than left to a keyword that does not exist. + // Python and JavaScript have no enumeration, so a member with no value of its own is given one + // rather than left to a keyword that does not exist. Assert.Contains("Static = 0", new PythonGenerator().Generate(kind), StringComparison.Ordinal); Assert.Contains("Static: 0,", new JavaScriptGenerator().Generate(kind), StringComparison.Ordinal); Assert.Contains("Object.freeze({", new JavaScriptGenerator().Generate(kind), StringComparison.Ordinal); } + /// + /// A member with no value of its own follows the one before it, as every language with real + /// enumerations numbers it, rather than taking its position — which would disagree with those + /// languages and could repeat an earlier member's value. + /// + [TestMethod] + public void Enum_NumbersAMemberAfterAnExplicitValueFromThatValue() + { + EnumDeclaration colour = new("Colour"); + colour.Members.Add(new EnumMember("Red")); + colour.Members.Add(new EnumMember("Green") { Value = "5" }); + colour.Members.Add(new EnumMember("Blue")); + + string python = new PythonGenerator().Generate(colour); + Assert.Contains("Red = 0", python, StringComparison.Ordinal); + Assert.Contains("Green = 5", python, StringComparison.Ordinal); + Assert.Contains("Blue = 6", python, StringComparison.Ordinal); + + string javaScript = new JavaScriptGenerator().Generate(colour); + Assert.Contains("Red: 0,", javaScript, StringComparison.Ordinal); + Assert.Contains("Green: 5,", javaScript, StringComparison.Ordinal); + Assert.Contains("Blue: 6,", javaScript, StringComparison.Ordinal); + } + + /// + /// A member following a value that is not a plain number counts on from that value's expression, + /// since the generator cannot work out what it comes to. + /// + [TestMethod] + public void Enum_CountsOnFromAnExplicitValueThatIsNotANumber() + { + EnumDeclaration flags = new("Flags"); + flags.Members.Add(new EnumMember("High") { Value = "1 << 4" }); + flags.Members.Add(new EnumMember("Next")); + flags.Members.Add(new EnumMember("After")); + + string python = new PythonGenerator().Generate(flags); + Assert.Contains("Next = (1 << 4) + 1", python, StringComparison.Ordinal); + Assert.Contains("After = (1 << 4) + 2", python, StringComparison.Ordinal); + + string javaScript = new JavaScriptGenerator().Generate(flags); + Assert.Contains("Next: (1 << 4) + 1,", javaScript, StringComparison.Ordinal); + Assert.Contains("After: (1 << 4) + 2,", javaScript, StringComparison.Ordinal); + } + /// /// A class body is not a block, so the namespace-scope spelling of an enumeration cannot simply /// be nested inside one. diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index a05b963..2a22986 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -3,7 +3,6 @@ namespace ktsu.Coder.Languages; using System.Collections.ObjectModel; -using System.Globalization; using System.Linq; using ktsu.Coder.Ast; using ktsu.CodeBlocker; @@ -137,10 +136,8 @@ private void GenerateNestedEnum(EnumDeclaration enumDecl, CodeBlocker code) /// The writer to emit into. private static void WriteEnumMembers(EnumDeclaration enumDecl, CodeBlocker code) { - for (int index = 0; index < enumDecl.Members.Count; index++) + foreach ((EnumMember member, string value) in enumDecl.Members.Zip(EnumMemberValues(enumDecl))) { - EnumMember member = enumDecl.Members[index]; - string value = member.Value ?? index.ToString(CultureInfo.InvariantCulture); code.WriteLine($"{member.Name ?? "UNNAMED"}: {value},"); } } diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index a0df1f6..924c325 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -792,4 +792,44 @@ protected static string EscapeString(string value) /// The operator has no mapping. /// Every language the generators target spells these identically. protected static string GetAssignmentOperator(AssignmentOperator op) => OperatorSymbols.GetSymbol(op); + + /// + /// Gives the value each member of an enumeration stands for, for a language that has to write + /// every one out. + /// + /// The declaration whose members to number. + /// One value per member, in order. + /// + /// A member with no value of its own is one more than the member before it, which is what every + /// language with real enumerations gives it; numbering it from its position instead would disagree + /// with those languages once any member has a value, and could repeat one. When the value being + /// counted on from is not a plain integer, it is counted on from as an expression, since the + /// generator cannot work out what it comes to. + /// + protected static IEnumerable EnumMemberValues(EnumDeclaration enumDecl) + { + Ensure.NotNull(enumDecl); + + string? origin = null; + long offset = 0; + + foreach (EnumMember member in enumDecl.Members) + { + if (member.Value is not null) + { + bool isNumber = long.TryParse(member.Value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out long number); + origin = isNumber ? null : member.Value; + offset = isNumber ? number : 0; + yield return member.Value; + } + else + { + yield return origin is null + ? offset.ToString(CultureInfo.InvariantCulture) + : $"({origin}) + {offset.ToString(CultureInfo.InvariantCulture)}"; + } + + offset++; + } + } } diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs index 2e60f6e..13a95d1 100644 --- a/Coder/Languages/PythonGenerator.cs +++ b/Coder/Languages/PythonGenerator.cs @@ -3,7 +3,6 @@ namespace ktsu.Coder.Languages; using System.Collections.ObjectModel; -using System.Globalization; using System.Linq; using ktsu.Coder.Ast; using ktsu.CodeBlocker; @@ -277,8 +276,8 @@ private void WriteArguments(ConstructionExpression construction, CodeBlocker cod /// /// /// Python has no enumeration syntax; enum.Enum is a class. A member with no value of its - /// own is numbered from its position, matching what a language with real enumerations would give - /// it. The from enum import Enum this needs belongs to the file rather than to the + /// own is one more than the member before it, matching what a language with real enumerations + /// would give it; see . The from enum import Enum this needs belongs to the file rather than to the /// declaration. /// protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBlocker code) @@ -296,10 +295,8 @@ protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBl return; } - for (int index = 0; index < enumDecl.Members.Count; index++) + foreach ((EnumMember member, string value) in enumDecl.Members.Zip(EnumMemberValues(enumDecl))) { - EnumMember member = enumDecl.Members[index]; - string value = member.Value ?? index.ToString(CultureInfo.InvariantCulture); code.WriteLine($"{member.Name ?? "UNNAMED"} = {value}"); } }