Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Jint.Benchmark/BuiltinShapeBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,53 @@ public Engine EngineInitGenerators()
engine.Evaluate(_initGenerators);
return engine;
}

// Forces the function-only Prototype-derived prototypes shaped in this campaign to Initialize —
// dictionary on main, shape here. The delta vs EngineOnly is the aggregate per-realm storage overhead
// removed across them.
private static readonly Prepared<Script> _initPrototypes = Engine.PrepareScript(
"Promise.prototype.then; WeakMap.prototype.has; WeakSet.prototype.has; WeakRef.prototype.deref; FinalizationRegistry.prototype.register;"
+ "BigInt.prototype.toString; AggregateError.prototype.name; SuppressedError.prototype.constructor;"
+ "Uint8Array.prototype.setFromBase64; ShadowRealm.prototype.evaluate;"
+ "Object.getPrototypeOf(function*(){}).constructor; Object.getPrototypeOf(async function(){}).constructor;"
+ "Object.getPrototypeOf(async function*(){}).constructor;"
+ "Map.prototype.has; Symbol.prototype.toString; ArrayBuffer.prototype.slice;"
+ "DataView.prototype.getInt8; Iterator.prototype.toArray;"
+ "Temporal.Duration.prototype.toString; Temporal.PlainDate.prototype.toString;"
+ "Temporal.PlainDateTime.prototype.toString; Temporal.ZonedDateTime.prototype.toString;"
+ "Temporal.Instant.prototype.toString; Temporal.PlainTime.prototype.toString;"
+ "Temporal.PlainMonthDay.prototype.toString; Temporal.PlainYearMonth.prototype.toString;"
+ "Intl.Locale.prototype.toString; Intl.Collator.prototype.resolvedOptions;"
+ "Intl.DateTimeFormat.prototype.resolvedOptions; Intl.NumberFormat.prototype.resolvedOptions;"
+ "Array.prototype.map; Number.prototype.toFixed; Boolean.prototype.valueOf; Error.prototype.toString;"
+ "[].values().next; new Map().keys().next; new Set().values().next; ''[Symbol.iterator]().next;"
+ "String.prototype.slice; Date.prototype.getTime; Set.prototype.union; RegExp.prototype.test;");

[Benchmark]
public Engine EngineInitPrototypes()
{
var engine = new Engine();
engine.Evaluate(_initPrototypes);
return engine;
}

// Forces the shaped constructors to Initialize (touch a static member on each) — dictionary on main,
// shape here. Constructors are Function-derived; the delta vs EngineOnly is the per-realm constructor
// storage overhead removed.
private static readonly Prepared<Script> _initConstructors = Engine.PrepareScript(
"Object.keys; Array.isArray; ArrayBuffer.isView; Map.groupBy; Set.prototype; Symbol.for; Promise.resolve;"
+ "Date.now; BigInt.asIntN; String.fromCharCode; Proxy.revocable; Iterator.from;"
+ "Temporal.PlainDate.from; Temporal.Duration.from; Temporal.Instant.from; Temporal.PlainDateTime.from;"
+ "Temporal.PlainTime.from; Temporal.PlainYearMonth.from; Temporal.PlainMonthDay.from; Temporal.ZonedDateTime.from;"
+ "Intl.NumberFormat.supportedLocalesOf; Intl.Collator.supportedLocalesOf; Intl.DateTimeFormat.supportedLocalesOf;"
+ "Intl.ListFormat.supportedLocalesOf; Intl.PluralRules.supportedLocalesOf; Intl.RelativeTimeFormat.supportedLocalesOf;"
+ "Intl.Segmenter.supportedLocalesOf; Intl.DisplayNames.supportedLocalesOf; Intl.DurationFormat.supportedLocalesOf;");

[Benchmark]
public Engine EngineInitConstructors()
{
var engine = new Engine();
engine.Evaluate(_initConstructors);
return engine;
}
}
22 changes: 22 additions & 0 deletions Jint.SourceGenerators/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,5 +195,27 @@ public JsSymbolAccessorAttribute(string symbolName, AccessorKind kind = Accessor
public global::Jint.Runtime.Descriptors.PropertyFlag Flags { get; set; } = global::Jint.Runtime.Descriptors.PropertyFlag.Configurable;
}

// Class-level, repeats. Registers a string own property <Name> that shares the descriptor of another
// generated [JsFunction] member <Target> (which must exist on the same host), so the two names resolve
// to the very same function object — the spec function-identity aliases, e.g.
// [JsAlias("keys", "values")] // Set.prototype.keys === values
// [JsAlias("trimLeft", "trimStart")]
// [JsAlias("toGMTString", "toUTCString")]
// The alias appears after all other own properties in own-key order (matching the hand-written
// AddDangerous/SetProperty tail it replaces).
[global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true)]
[global::System.Diagnostics.Conditional("JINT_SOURCE_GENERATORS")]
internal sealed class JsAliasAttribute : global::System.Attribute
{
public JsAliasAttribute(string name, string target)
{
Name = name;
Target = target;
}

public string Name { get; }
public string Target { get; }
}

""";
}
86 changes: 74 additions & 12 deletions Jint.SourceGenerators/Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ public static string Emit(ObjectDefinition obj)

sb.Append(obj.Accessibility).Append(" ");
if (obj.IsSealed) sb.Append("sealed ");
sb.Append("partial class ").AppendLine(obj.Name);
sb.Append("partial class ").Append(obj.Name);
// A shaped host that does not derive from BuiltinShapeObject gets its IBuiltinShaped storage emitted
// here (see EmitShapeMembers); declare the interface on this partial so the field/impls satisfy it.
if (obj.UseShape && !obj.DerivesFromBuiltinShapeObject)
{
sb.Append(" : global::Jint.Native.Object.IBuiltinShaped");
}
sb.AppendLine();
sb.AppendLine("{");

EmitStaticPropertyDescriptors(sb, obj);
Expand Down Expand Up @@ -123,6 +130,7 @@ private static IEnumerable<string> EnumerateAllJsNames(ObjectDefinition obj, IRe
foreach (var ir in obj.IntrinsicReferences) yield return ir.JsName;
foreach (var name in accessorsByName.Keys) yield return name;
foreach (var thr in obj.ThrowerAccessors) yield return thr.JsName;
foreach (var a in obj.Aliases) yield return a.Name;
}

/// <summary>
Expand Down Expand Up @@ -170,7 +178,7 @@ private static void EmitInitialize(StringBuilder sb, ObjectDefinition obj)
}
}

var totalEntries = dataProperties.Count + obj.Properties.Count + accessorsByName.Count + obj.ThrowerAccessors.Count + obj.IntrinsicReferences.Count;
var totalEntries = dataProperties.Count + obj.Properties.Count + accessorsByName.Count + obj.ThrowerAccessors.Count + obj.IntrinsicReferences.Count + obj.Aliases.Count;

if (totalEntries > 0) EmitStaticKeyCache(sb, obj, dataProperties, accessorsByName);

Expand All @@ -194,7 +202,7 @@ private static void EmitInitialize(StringBuilder sb, ObjectDefinition obj)
sb.Append(prop.ClrName).Append(", ").Append(prop.FlagsExpression).AppendLine(");");
}
sb.AppendLine(" }");
EmitShapeMembers(sb, obj, dataProperties);
EmitShapeMembers(sb, obj, dataProperties, accessorsByName);
return;
}

Expand Down Expand Up @@ -319,6 +327,13 @@ private static void EmitInitialize(StringBuilder sb, ObjectDefinition obj)
}
}

// Aliases: share the target member's descriptor (added last, matching own-key order).
foreach (var alias in obj.Aliases)
{
sb.Append(" properties.TryGetValue(__Key_").Append(SanitizeIdentifier(alias.Target)).Append(", out var __alias_").Append(SanitizeIdentifier(alias.Name)).AppendLine(");");
sb.Append(" properties.AddDangerous(__Key_").Append(SanitizeIdentifier(alias.Name)).Append(", __alias_").Append(SanitizeIdentifier(alias.Name)).AppendLine(");");
}

sb.AppendLine(" SetProperties(properties);");
sb.AppendLine(" }");
}
Expand All @@ -330,19 +345,24 @@ private static void EmitInitialize(StringBuilder sb, ObjectDefinition obj)
/// after the static Key / descriptor fields in this same generated file, so it runs after them.
/// Supports [JsFunction]s, static-immutable [JsProperty] constants, and [JsSymbol]s.
/// </summary>
private static void EmitShapeMembers(StringBuilder sb, ObjectDefinition obj, List<FunctionDefinition> dataProperties)
private static void EmitShapeMembers(StringBuilder sb, ObjectDefinition obj, List<FunctionDefinition> dataProperties, IReadOnlyDictionary<string, (FunctionDefinition? Get, FunctionDefinition? Set, string Flags)> accessorsByName)
{
var dispatcher = "__" + obj.Name + "Function";
var singleSlot = obj.Functions.Count == 1;

// Slot expression for a getter/setter half: NotAFunction when absent, else the dispatcher slot id.
string SlotExpr(FunctionDefinition? fn) => fn is null
? "global::Jint.Native.Object.BuiltinShape.NotAFunction"
: singleSlot ? "(global::System.UInt16) 0" : "(global::System.UInt16) " + dispatcher + ".Slot." + fn.ClrName;

sb.AppendLine();
sb.AppendLine(" private static readonly global::Jint.Native.Object.BuiltinShape __builtinShape = BuildBuiltinShape_Generated();");
sb.AppendLine();
sb.AppendLine(" private static global::Jint.Native.Object.BuiltinShape BuildBuiltinShape_Generated()");
sb.AppendLine(" {");
sb.Append(" var builder = new global::Jint.Native.Object.BuiltinShape.Builder(").Append(obj.Properties.Count + dataProperties.Count).AppendLine(");");
// Properties first (matching the dictionary path's order): static-immutable constants share a
// process-wide descriptor; everything else is a per-realm instance slot filled in Initialize.
sb.Append(" var builder = new global::Jint.Native.Object.BuiltinShape.Builder(").Append(obj.Properties.Count + dataProperties.Count + accessorsByName.Count + obj.Aliases.Count).AppendLine(");");
// Order matches the dictionary path: properties, then functions, then accessors (each sorted by JsName).
// Static-immutable constants share a process-wide descriptor; other properties are per-realm instance slots.
foreach (var prop in obj.Properties)
{
if (prop.IsStatic && prop.IsImmutable)
Expand All @@ -361,14 +381,56 @@ private static void EmitShapeMembers(StringBuilder sb, ObjectDefinition obj, Lis
else sb.Append(dispatcher).Append(".Slot.").Append(fn.ClrName);
sb.Append(", ").Append(fn.FlagsExpression).AppendLine(");");
}
var accessorNames = new List<string>(accessorsByName.Keys);
accessorNames.Sort(StringComparer.Ordinal);
foreach (var name in accessorNames)
{
var (get, set, flags) = accessorsByName[name];
sb.Append(" builder.Accessor(__Key_").Append(SanitizeIdentifier(name)).Append(", ")
.Append(SlotExpr(get)).Append(", ").Append(SlotExpr(set)).Append(", ").Append(flags).AppendLine(");");
}
// Aliases last (matching own-key order); each shares its already-added target slot's descriptor.
foreach (var alias in obj.Aliases)
{
sb.Append(" builder.Alias(__Key_").Append(SanitizeIdentifier(alias.Name)).Append(", __Key_").Append(SanitizeIdentifier(alias.Target)).AppendLine(");");
}
sb.AppendLine(" return builder.Build();");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private protected override global::Jint.Native.Object.BuiltinShape BuiltinShape => __builtinShape;");
sb.AppendLine();
sb.Append(" private protected override global::Jint.Native.Function.Function MakeBuiltinFunction(global::System.UInt16 slot) => new ").Append(dispatcher).Append("(this");
if (!singleSlot) sb.Append(", (").Append(dispatcher).Append(".Slot) slot");
sb.AppendLine(");");

// A shaped host with no [JsFunction]s (only instance/constant properties + symbols) has no dispatcher
// and no function slots, so MakeBuiltinFunction is never invoked — emit a throwing body rather than
// reference a dispatcher type that isn't generated.
string makeBody;
if (obj.Functions.Count == 0)
{
makeBody = "=> throw new global::System.InvalidOperationException(\"" + obj.Name + " has no built-in function slots\")";
}
else
{
makeBody = "=> new " + dispatcher + "(this" + (singleSlot ? "" : ", (" + dispatcher + ".Slot) slot") + ")";
}

if (obj.DerivesFromBuiltinShapeObject)
{
// Storage (the per-realm descriptor array + IBuiltinShaped) comes from the BuiltinShapeObject base;
// just supply the two abstract hooks it declares.
sb.AppendLine(" private protected override global::Jint.Native.Object.BuiltinShape BuiltinShape => __builtinShape;");
sb.AppendLine();
sb.Append(" private protected override global::Jint.Native.Function.Function MakeBuiltinFunction(global::System.UInt16 slot) ").Append(makeBody).AppendLine(";");
}
else
{
// Non-BuiltinShapeObject host (prototype / constructor): emit the per-realm descriptor field and the
// IBuiltinShaped implementation here, so the shape store composes without a shared base class.
sb.AppendLine(" private global::Jint.Runtime.Descriptors.PropertyDescriptor?[]? __builtinDescriptors;");
sb.AppendLine();
sb.AppendLine(" global::Jint.Native.Object.BuiltinShape global::Jint.Native.Object.IBuiltinShaped.BuiltinShape => __builtinShape;");
sb.AppendLine();
sb.AppendLine(" global::Jint.Runtime.Descriptors.PropertyDescriptor?[]? global::Jint.Native.Object.IBuiltinShaped.BuiltinDescriptors { get => __builtinDescriptors; set => __builtinDescriptors = value; }");
sb.AppendLine();
sb.Append(" global::Jint.Native.Function.Function global::Jint.Native.Object.IBuiltinShaped.MakeBuiltinFunction(global::System.UInt16 slot) ").Append(makeBody).AppendLine(";");
}
}

private static void EmitDispatcher(StringBuilder sb, ObjectDefinition obj)
Expand Down
44 changes: 31 additions & 13 deletions Jint.SourceGenerators/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ internal sealed record class ObjectDefinition(
bool IsSealed,
int ExtraCapacity,
bool UseShape,
bool DerivesFromBuiltinShapeObject,
EquatableArray<FunctionDefinition> Functions,
EquatableArray<PropertyDefinition> Properties,
EquatableArray<SymbolDefinition> Symbols,
EquatableArray<ThrowerAccessorDefinition> ThrowerAccessors,
EquatableArray<IntrinsicReferenceDefinition> IntrinsicReferences,
EquatableArray<AliasDefinition> Aliases,
EquatableArray<DiagnosticInfo> DiagnosticInfos)
{
public string HintName => string.IsNullOrEmpty(Namespace)
Expand Down Expand Up @@ -103,13 +105,14 @@ public IEnumerable<Diagnostic> Diagnostics
// explicit opt-out. Hosts that don't derive from it always use the dictionary path regardless.
var extraCapacity = 0;
var useShapeRequested = true;
var useShapeExplicitlySet = false;
foreach (var attr in typeSymbol.GetAttributes())
{
if (attr.AttributeClass?.Name != "JsObjectAttribute") continue;
foreach (var named in attr.NamedArguments)
{
if (named.Key == "ExtraCapacity" && named.Value.Value is int v) extraCapacity = v;
else if (named.Key == "UseShape" && named.Value.Value is bool s) useShapeRequested = s;
else if (named.Key == "UseShape" && named.Value.Value is bool s) { useShapeRequested = s; useShapeExplicitlySet = true; }
}
}

Expand All @@ -122,13 +125,19 @@ public IEnumerable<Diagnostic> Diagnostics
break;
}
}
var useShape = useShapeRequested && derivesFromBuiltinShapeObject;
// Two opt-ins: deriving from BuiltinShapeObject enables shapes by default (UseShape = false opts out);
// a host on any other base (a prototype / constructor) opts in with an explicit [JsObject(UseShape = true)],
// in which case the generator emits the IBuiltinShaped storage glue onto the host itself.
var useShape = derivesFromBuiltinShapeObject
? useShapeRequested
: (useShapeExplicitlySet && useShapeRequested);

var functions = new List<FunctionDefinition>();
var properties = new List<PropertyDefinition>();
var symbols = new List<SymbolDefinition>();
var throwerAccessors = new List<ThrowerAccessorDefinition>();
var intrinsicReferences = new List<IntrinsicReferenceDefinition>();
var aliases = new List<AliasDefinition>();
HashSet<string>? seenFunctionClrNames = null;
HashSet<string>? seenThrowerNames = null;
HashSet<string>? seenIntrinsicReferenceNames = null;
Expand Down Expand Up @@ -186,6 +195,17 @@ public IEnumerable<Diagnostic> Diagnostics
FlagsExpression: FlagExpression.From(refFlagsExplicit, "Configurable")));
}

// Class-level [JsAlias("name", "target")] — a string property that shares another member's descriptor.
foreach (var attr in typeSymbol.GetAttributes())
{
if (attr.AttributeClass?.Name != "JsAliasAttribute") continue;
if (attr.ConstructorArguments.Length < 2) continue;
var aliasName = attr.ConstructorArguments[0].Value as string;
var aliasTarget = attr.ConstructorArguments[1].Value as string;
if (string.IsNullOrWhiteSpace(aliasName) || string.IsNullOrWhiteSpace(aliasTarget)) continue;
aliases.Add(new AliasDefinition(aliasName!, aliasTarget!));
}

foreach (var member in typeSymbol.GetMembers())
{
ct.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -347,17 +367,11 @@ public IEnumerable<Diagnostic> Diagnostics
// representable yet — fail loudly rather than silently dropping them.
if (useShape)
{
var hasUnsupported = intrinsicReferences.Count > 0 || throwerAccessors.Count > 0;
foreach (var fn in functions)
{
if (fn.Registration is RegistrationKind.AccessorGet or RegistrationKind.AccessorSet
or RegistrationKind.SymbolAccessorGet or RegistrationKind.SymbolAccessorSet)
{
hasUnsupported = true;
break;
}
}
if (hasUnsupported)
// The shape path supports [JsFunction]s, static-immutable constants + per-realm instance
// properties, symbols, string [JsAccessor]s (a GetSetPropertyDescriptor slot), and symbol
// accessors (which register in the symbol dictionary, orthogonal to the string shape).
// Intrinsic references and thrower accessors are not representable yet — fail loudly.
if (intrinsicReferences.Count > 0 || throwerAccessors.Count > 0)
{
diagnostics.Add(new DiagnosticInfo(DiagnosticDescriptors.UnsupportedShapeMember, location, typeSymbol.Name));
}
Expand All @@ -376,11 +390,13 @@ public IEnumerable<Diagnostic> Diagnostics
IsSealed: typeSymbol.IsSealed,
ExtraCapacity: extraCapacity,
UseShape: useShape,
DerivesFromBuiltinShapeObject: derivesFromBuiltinShapeObject,
Functions: functions.ToEquatableArray(),
Properties: properties.ToEquatableArray(),
Symbols: symbols.ToEquatableArray(),
ThrowerAccessors: throwerAccessors.ToEquatableArray(),
IntrinsicReferences: intrinsicReferences.ToEquatableArray(),
Aliases: aliases.ToEquatableArray(),
DiagnosticInfos: diagnostics.ToEquatableArray());
}

Expand Down Expand Up @@ -490,6 +506,8 @@ internal sealed record class ThrowerAccessorDefinition(string JsName, string Fla

internal sealed record class IntrinsicReferenceDefinition(string JsName, string IntrinsicMember, string FlagsExpression);

internal sealed record class AliasDefinition(string Name, string Target);

internal sealed record class FunctionDefinition(
string ClrName,
string JsName,
Expand Down
Loading
Loading