What's wrong
JavaScriptGenerator.GenerateFieldDeclaration (Coder/Languages/JavaScriptGenerator.cs:273-287) writes name = value; and nothing else. It never reads IsStatic or IsConstant, and it writes the same text wherever the field stands:
- Inside a class, a
static or constant field becomes an ordinary instance field. Counter.MAX is undefined, and every instance gets its own copy of what was meant to be shared state. The VariableDeclaration-as-field path in the same file (GenerateField, :391-409) already writes static for a constant, with a comment explaining why. The FieldDeclaration path, which is the node the AST documents for fields, does not.
- At file or namespace scope, the declaration has no
const/let, so it is a bare assignment to an undeclared name. The generator writes imports as ES module imports (SpellImport at :107 gives import "...";), and modules are always strict. In a module that assignment throws ReferenceError at load. In a sloppy-mode script it silently creates a global instead.
The other targets all honour these flags for the same AST:
| Target |
File-scope constant |
Static field |
| C++ |
inline constexpr int Limit = 10; |
inline static int created = 0; |
| C# |
— |
public static int created / static readonly |
| Go |
const Limit int = 10 |
var CounterCreated int = 0 |
Rust's equivalent gap is tracked separately in #81; this issue is JavaScript only.
Reproduction
var file = new SourceFile("Sample");
file.Members.Add(new FieldDeclaration("Limit", "int") { IsConstant = true, InitialValue = new LiteralExpression<int>(10) });
var counter = new ClassDeclaration("Counter");
counter.Members.Add(new FieldDeclaration("created", "int") { IsStatic = true, InitialValue = new LiteralExpression<int>(0) });
counter.Members.Add(new FieldDeclaration("MAX", "int") { IsStatic = true, IsConstant = true, InitialValue = new LiteralExpression<int>(99) });
file.Members.Add(counter);
Console.WriteLine(new JavaScriptGenerator().Generate(file));
Actual output:
Limit = 10;
class Counter {
created = 0;
MAX = 99;
}
Run as an ES module (node gen.mjs), node 22 fails on line 1:
Limit = 10;
^
ReferenceError: Limit is not defined
Run as a classic script, it loads, but console.log(Counter.MAX, globalThis.Limit) prints undefined 10. The static field is missing from the class, and the file-scope constant leaked onto the global object.
Expected:
const Limit = 10;
class Counter {
static created = 0;
static MAX = 99;
}
Why it matters
Constant tables and shared counters are among the main things the AST is documented to generate (see FieldDeclaration.IsStatic/IsConstant in CLAUDE.md). In JavaScript they either crash the module at load, or quietly change from one shared value to one value per instance.
Suggested fix / acceptance criteria
- Inside a class body, write
static when IsStatic || IsConstant, matching GenerateField for VariableDeclaration.
- Outside a class, meaning directly in a
SourceFile or NamespaceDeclaration, write const for IsConstant and let otherwise, as GenerateVariableDeclaration already does for locals.
- Add tests covering: a static field in a class, a constant field in a class, and a constant field at file scope. Ideally run the output with node when it is available and check
Counter.MAX === 99.
What's wrong
JavaScriptGenerator.GenerateFieldDeclaration(Coder/Languages/JavaScriptGenerator.cs:273-287) writesname = value;and nothing else. It never readsIsStaticorIsConstant, and it writes the same text wherever the field stands:staticor constant field becomes an ordinary instance field.Counter.MAXisundefined, and every instance gets its own copy of what was meant to be shared state. The VariableDeclaration-as-field path in the same file (GenerateField,:391-409) already writesstaticfor a constant, with a comment explaining why. TheFieldDeclarationpath, which is the node the AST documents for fields, does not.const/let, so it is a bare assignment to an undeclared name. The generator writes imports as ES module imports (SpellImportat:107givesimport "...";), and modules are always strict. In a module that assignment throwsReferenceErrorat load. In a sloppy-mode script it silently creates a global instead.The other targets all honour these flags for the same AST:
inline constexpr int Limit = 10;inline static int created = 0;public static int created/static readonlyconst Limit int = 10var CounterCreated int = 0Rust's equivalent gap is tracked separately in #81; this issue is JavaScript only.
Reproduction
Actual output:
Run as an ES module (
node gen.mjs), node 22 fails on line 1:Run as a classic script, it loads, but
console.log(Counter.MAX, globalThis.Limit)printsundefined 10. The static field is missing from the class, and the file-scope constant leaked onto the global object.Expected:
Why it matters
Constant tables and shared counters are among the main things the AST is documented to generate (see
FieldDeclaration.IsStatic/IsConstantin CLAUDE.md). In JavaScript they either crash the module at load, or quietly change from one shared value to one value per instance.Suggested fix / acceptance criteria
staticwhenIsStatic || IsConstant, matchingGenerateFieldforVariableDeclaration.SourceFileorNamespaceDeclaration, writeconstforIsConstantandletotherwise, asGenerateVariableDeclarationalready does for locals.Counter.MAX === 99.