-
Notifications
You must be signed in to change notification settings - Fork 1
Journey 06 Source Generators
November 30, 2025 - I got tired of typing
JavaScript has a lot of built-in methods. Like, a LOT:
- Array:
push,pop,map,filter,reduce,forEach,find,findIndex,includes,indexOf,slice,splice... (30+ more) - String:
charAt,charCodeAt,concat,includes,indexOf,slice,split,substring,toLowerCase,toUpperCase... (40+ more) - Object:
keys,values,entries,assign,freeze,seal,create,defineProperty... (20+ more)
Each method needs:
- Argument validation
- Type coercion
- The actual logic
- Return value wrapping
- Error handling
Writing this boilerplate for hundreds of methods? No thanks.
Roslyn source generators. Write attributes, get code.
[JsPrototype("Array")]
public partial class ArrayPrototype
{
[JsMethod("push")]
public static JsValue Push(JsValue thisArg, JsValue[] args)
{
var array = thisArg.AsArray();
foreach (var arg in args)
array.Add(arg);
return JsValue.FromNumber(array.Length);
}
[JsMethod("pop")]
public static JsValue Pop(JsValue thisArg, JsValue[] args)
{
var array = thisArg.AsArray();
if (array.Length == 0)
return JsValue.Undefined;
var value = array[^1];
array.RemoveAt(array.Length - 1);
return value;
}
}The source generator sees these attributes and generates all the wiring code:
// Generated by PrototypeSourceGenerator
public partial class ArrayPrototype
{
public static void Initialize(Realm realm)
{
var proto = realm.Intrinsics.ArrayPrototype;
proto.DefineOwnProperty("push", new PropertyDescriptor(
value: new JsHostFunction("push", Push, 1),
writable: true,
enumerable: false,
configurable: true
));
proto.DefineOwnProperty("pop", new PropertyDescriptor(
value: new JsHostFunction("pop", Pop, 0),
writable: true,
enumerable: false,
configurable: true
));
// ... generated for every [JsMethod]
}
}Write the logic. Skip the ceremony.
Source generators are basically just Roslyn plugins that run at compile time:
Your Code → Roslyn Compiler → Source Generator → Generated Code
↓
Combined Assembly
The generator:
- Finds classes with
[JsPrototype] - Finds methods with
[JsMethod],[JsGetter],[JsStaticMethod] - Generates initialization code
- Outputs
.g.csfiles that become part of your assembly
[Generator]
public class PrototypeSourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Find [JsPrototype] classes
// For each, find [JsMethod] methods
// Generate Initialize() method
// Output the source
}
}- Before: ~50 lines per method (with wiring)
- After: ~10 lines per method (just the logic)
- Methods in stdlib: 200+
- Lines saved: Thousands
[JsConstructor("Array")]
public partial class ArrayConstructor
{
[JsConstruct]
public static JsValue Construct(JsValue newTarget, JsValue[] args)
{
if (args.Length == 0)
return JsValue.FromObject(new JsArray());
if (args.Length == 1 && args[0].IsNumber)
return JsValue.FromObject(new JsArray((int)args[0].AsNumber()));
var array = new JsArray();
foreach (var arg in args)
array.Add(arg);
return JsValue.FromObject(array);
}
[JsStaticMethod("isArray")]
public static JsValue IsArray(JsValue thisArg, JsValue[] args)
{
return JsValue.FromBoolean(
args.Length > 0 && args[0].IsArray
);
}
}The generator wires up:
-
[[Construct]]behavior (called withnew) -
[[Call]]behavior (called withoutnew) - Static methods on the constructor function
The generator validates at compile time:
// Error: Method must be static
[JsMethod("push")]
public JsValue Push(...) { } // Missing 'static'
// Error: Wrong signature
[JsMethod("push")]
public static void Push() { } // Must return JsValueMistakes caught before runtime. Beautiful.
Implementing a JavaScript stdlib is a slog. You're writing the same patterns over and over:
- Check argument count
- Coerce
thisto the right type - Coerce arguments
- Do the thing
- Return a JsValue
Source generators let us focus on step 4 and automate the rest.
The Internationalization API was a perfect fit:
[JsPrototype("Intl.NumberFormat")]
public partial class NumberFormatPrototype
{
[JsMethod("format")]
public static JsValue Format(JsValue thisArg, JsValue[] args)
{
var formatter = thisArg.GetInternalSlot<NumberFormatter>();
var number = args.Length > 0 ? args[0].ToNumber() : double.NaN;
return JsValue.FromString(formatter.Format(number));
}
[JsGetter("resolvedOptions")]
public static JsValue GetResolvedOptions(JsValue thisArg)
{
// Return the options object
}
}All the locale handling, format strings, and options parsing in one place. The wiring is invisible.
- Automate the boring stuff - Generators are perfect for boilerplate
- Compile-time > runtime - Catch mistakes early
- Attribute-based APIs are clean - Mark it, it works
- Consistency for free - Generated code follows the same pattern every time
In short: writing Asynkron.JsEngine.Generators took a couple days. It's saved weeks of boilerplate since.
Previous: Chapter 5: TypedAST Next: Chapter 7: Async Remake - Time to admit CPS wasn't cutting it
//Roger