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
14 changes: 13 additions & 1 deletion Jint/Runtime/Descriptors/Specialized/LazyPropertyDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,19 @@ internal LazyPropertyDescriptor(T state, Func<T, JsValue> resolver, PropertyFlag

protected internal override JsValue? CustomValue
{
get => _value ??= _resolver(_state);
get
{
var value = _value;
if (value is null)
{
_value = value = _resolver(_state);
// Once materialized this is semantically a plain data descriptor; clearing the
// flag lets value reads/writes skip the CustomValue indirection and admits the
// descriptor to the global-binding and member-write inline caches.
_flags &= ~PropertyFlag.CustomJsValue;
}
return value;
}
set => _value = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -962,8 +962,10 @@ private JsValue SetValue(EvaluationContext context)
// identifiers to a single field test; the rest stays out-of-line.
if (left._cachedGlobalEnv is not null)
{
// Writable is re-checked through the cached reference: defineProperty flips the
// flag in place without bumping the versions the validator checks.
var cachedGlobalDescriptor = left.TryGetValidatedGlobalDescriptor(engine, env);
if (cachedGlobalDescriptor is not null)
if (cachedGlobalDescriptor is not null && cachedGlobalDescriptor.Writable)
{
return AssignToCachedGlobalBinding(context, left, right, cachedGlobalDescriptor, hasEvalOrArguments, nameAnonymousFunction, strict);
}
Expand Down Expand Up @@ -1011,8 +1013,9 @@ private JsValue SetValue(EvaluationContext context)

// Populate the global-binding cache from the write side too, so write-first
// patterns benefit from the next access on. Must run after the set: the set
// may have created the property (bumping the shape version).
if (ReferenceEquals(environmentRecord, env) && environmentRecord is GlobalEnvironment globalEnv)
// may have created the property (bumping the shape version). No hop
// restriction — the validator re-walks with shadow probes on every use.
if (environmentRecord is GlobalEnvironment globalEnv)
{
left.TryRememberGlobalBinding(globalEnv);
}
Expand Down
96 changes: 76 additions & 20 deletions Jint/Runtime/Interpreter/Expressions/JintIdentifierExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ internal sealed class JintIdentifierExpression : JintExpression
private DeclarativeEnvironment? _cachedSlotEnv;
private int _cachedSlotIndex = -1;

// Version-based inline cache for global bindings: when top-level code (current lexical
// env IS the global env) resolves to a plain writable MutableBinding data property on the
// real GlobalObject, remember the descriptor. Valid while the global object's own-property
// shape and the set of global lexical declarations are unchanged — value writes mutate the
// Version-based inline cache for global bindings: when resolution (from any scope depth)
// lands on a plain writable data property of the real GlobalObject, remember the
// descriptor. Valid while the global object's own-property shape and the set of global
// lexical declarations are unchanged AND a bounded walk from the current env reaches the
// global env without an intermediate that could own the name — value writes mutate the
// descriptor in place and bump neither version. Mirrors JintMemberExpression's read cache.
internal GlobalEnvironment? _cachedGlobalEnv;
private Runtime.Descriptors.PropertyDescriptor? _cachedGlobalDescriptor;
Expand Down Expand Up @@ -232,8 +233,10 @@ public override JsValue GetValue(EvaluationContext context)
_cachedSlotIndex = slotIndex;
}
}
else if (ReferenceEquals(identifierEnvironment, env) && identifierEnvironment is GlobalEnvironment globalEnv)
else if (identifierEnvironment is GlobalEnvironment globalEnv)
{
// No hop restriction: the validator re-walks with shadow probes on every read,
// so nested-scope reads of globals (loop bodies, closures) can use the cache.
TryRememberGlobalBinding(globalEnv);
}

Expand All @@ -252,31 +255,84 @@ public override JsValue GetValue(EvaluationContext context)
}

/// <summary>
/// Validates the global-binding cache for the current environment: the current lexical env
/// must be the cached GlobalEnvironment itself (top-level code) and neither the global
/// object's own-property shape nor the global lexical declaration set may have changed.
/// Returns the cached plain writable data descriptor, or null on miss.
/// Validates the global-binding cache for the current environment: neither the global
/// object's own-property shape nor the global lexical declaration set may have changed,
/// and a bounded walk from the current lexical env must reach the cached GlobalEnvironment
/// without passing an environment that could own the name (mirrors the slot-cache walk:
/// with-objects and declarative envs holding the binding — e.g. sloppy direct eval injected
/// a var — bail to full resolution; the per-read probes are what make nested-scope use
/// safe). Returns the cached plain writable data descriptor, or null on miss.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
internal Runtime.Descriptors.PropertyDescriptor? TryGetValidatedGlobalDescriptor(Engine engine, Environment env)
{
var cachedGlobalEnv = _cachedGlobalEnv;
if (cachedGlobalEnv is not null
&& ReferenceEquals(env, cachedGlobalEnv)
&& ReferenceEquals(cachedGlobalEnv._engine, engine)
&& cachedGlobalEnv._global._propertiesVersion == _cachedGlobalShapeVersion
&& cachedGlobalEnv._lexicalMutations == _cachedGlobalLexicalVersion)
if (cachedGlobalEnv is null)
{
return _cachedGlobalDescriptor;
return null;
}

// Hop 0 (top-level code) keeps the single identity compare up front and this method
// compact — it is by far the most common case (var-heavy scripts validate here many
// times per loop iteration) and must not pay for the nested-scope walk's code size.
if (ReferenceEquals(env, cachedGlobalEnv))
{
return ReferenceEquals(cachedGlobalEnv._engine, engine)
&& cachedGlobalEnv._global._propertiesVersion == _cachedGlobalShapeVersion
&& cachedGlobalEnv._lexicalMutations == _cachedGlobalLexicalVersion
? _cachedGlobalDescriptor
: null;
}

return TryGetValidatedGlobalDescriptorNested(engine, env, cachedGlobalEnv);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private Runtime.Descriptors.PropertyDescriptor? TryGetValidatedGlobalDescriptorNested(Engine engine, Environment env, GlobalEnvironment cachedGlobalEnv)
{
if (!ReferenceEquals(cachedGlobalEnv._engine, engine)
|| cachedGlobalEnv._global._propertiesVersion != _cachedGlobalShapeVersion
|| cachedGlobalEnv._lexicalMutations != _cachedGlobalLexicalVersion)
{
return null;
}

var key = _identifier.Key;
var search = env;
for (var hops = 0; hops < MaxSlotCacheChainDepth; hops++)
{
if (search is ObjectEnvironment)
{
return null;
}

if (search is DeclarativeEnvironment intermediate && intermediate.HasBinding(key))
{
return null;
}

search = search._outerEnv;
if (search is null)
{
return null;
}

if (ReferenceEquals(search, cachedGlobalEnv))
{
return _cachedGlobalDescriptor;
}
}

return null;
}

/// <summary>
/// Caches the resolved global binding when it is a plain writable MutableBinding data
/// property on the real GlobalObject and no lexical declaration shadows it. Both reads
/// and writes may then operate on the descriptor directly while the versions hold.
/// Caches the resolved global binding when it is a plain writable data property on the
/// real GlobalObject (no CustomValue indirection — materialized lazy built-ins qualify,
/// live accessor-like descriptors never do) and no lexical declaration shadows it. Reads
/// may then use the descriptor directly while the versions hold; writers must re-check
/// <see cref="Runtime.Descriptors.PropertyDescriptor.Writable"/> through the cached
/// reference because defineProperty flips flags in place without bumping the versions.
/// </summary>
internal void TryRememberGlobalBinding(GlobalEnvironment globalEnv)
{
Expand All @@ -287,10 +343,10 @@ internal void TryRememberGlobalBinding(GlobalEnvironment globalEnv)
return;
}

if (globalObject._properties!.TryGetValue(identifier.Key, out var descriptor)
if (globalObject._properties?.TryGetValue(identifier.Key, out var descriptor) == true
&& descriptor.IsDataDescriptor()
&& descriptor.Writable
&& (descriptor._flags & Runtime.Descriptors.PropertyFlag.MutableBinding) != Runtime.Descriptors.PropertyFlag.None)
&& (descriptor._flags & Runtime.Descriptors.PropertyFlag.CustomJsValue) == Runtime.Descriptors.PropertyFlag.None)
{
_cachedGlobalEnv = globalEnv;
_cachedGlobalDescriptor = descriptor;
Expand Down
4 changes: 3 additions & 1 deletion Jint/Runtime/Interpreter/Expressions/JintUpdateExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ private JsValue UpdateNonIdentifier(EvaluationContext context)
if (_leftIdentifier!._cachedGlobalEnv is not null && !context.OperatorOverloadingAllowed)
{
var descriptor = _leftIdentifier.TryGetValidatedGlobalDescriptor(engine, engine.ExecutionContext.LexicalEnvironment);
if (descriptor is not null && descriptor._value is { } current)
// Writable is re-checked through the cached reference: defineProperty flips the
// flag in place without bumping the versions the validator checks.
if (descriptor is not null && descriptor.Writable && descriptor._value is { } current)
{
if (_evalOrArguments && StrictModeScope.IsStrictModeCode)
{
Expand Down
Loading