Browse Source

Declare the reference resolver's interests to re-arm the read caches

Registering an IReferenceResolver without interests gives it
ReferenceResolverInterests.All, and two of those flags - ObjectPropertyBase
and PrimitivePropertyBase - are the gate on the non-computed member-read
inline caches, the dense-array indexed-read lane and the member-call callee
lane. With All declared, every property read in every script has to be
routed through a Reference so the resolver gets offered the base, and all
three lanes stay off for the whole engine.

NullPropagation.TryPropertyReference returns false for every base that is
not null or undefined, so on those two situations the engine consulting it
can never change the result. Declaring only the three situations the
resolver actually answers - NullishPropertyBase, UnresolvableReference and
NonCallableCallee - is therefore observably identical and re-arms all three
lanes. Interests are documented as a subscription filter and not a promise:
a situation that is not subscribed to behaves exactly as if no resolver
were registered.

Jint also ships a built-in NullPropagatingReferenceResolver, which is
deliberately NOT adopted here: it declines unresolvable identifiers and
non-callable callees, where this resolver answers both, so swapping it in
would turn an undeclared-name read and a call on a nullish chain into
errors for existing tenant scripts.

The behaviour pins from the first commit cover exactly those edges and all
156 tests in Operations/Scripting still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f
pull/1326/head
Marko Lahma 1 month ago
parent
commit
a751cab46a
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  2. 30
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -146,7 +146,7 @@ public sealed class JintScriptEngine(IMemoryCache cache, IOptions<JintScriptOpti
engineOptions.AddObjectConverter(JintObjectConverter.Instance);
engineOptions.AllowClrWrite(!options.Readonly);
engineOptions.SetTypeConverter(engine => new CustomClrConverter(engine));
engineOptions.SetReferencesResolver(NullPropagation.Instance);
engineOptions.SetReferencesResolver(NullPropagation.Instance, NullPropagation.Interests);
engineOptions.Strict();
if (!Debugger.IsAttached)

30
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs

@ -14,8 +14,38 @@ namespace Squidex.Domain.Apps.Core.Scripting;
public sealed class NullPropagation : IReferenceResolver
{
/// <summary>
/// The situations this resolver actually answers, declared so the engine keeps the fast paths for
/// everything else.
/// </summary>
/// <remarks>
/// Deliberately omitted are <see cref="ReferenceResolverInterests.ObjectPropertyBase"/> and
/// <see cref="ReferenceResolverInterests.PrimitivePropertyBase"/>, the pair that disables the
/// non-computed member-read inline caches, the dense-array indexed-read lane and the member-call callee
/// lane engine-wide. <see cref="TryPropertyReference"/> declines every base that is not null or
/// undefined, so those are situations where the engine consulting this resolver could never change the
/// result. Interests are a subscription filter and not a promise: a situation not subscribed to behaves
/// exactly as if no resolver were registered.
/// </remarks>
public const ReferenceResolverInterests Interests =
ReferenceResolverInterests.NullishPropertyBase |
ReferenceResolverInterests.UnresolvableReference |
ReferenceResolverInterests.NonCallableCallee;
public static readonly NullPropagation Instance = new NullPropagation();
/// <summary>
/// Answers a read of a name that resolves to no binding, so that an unknown name does not throw a
/// reference error.
/// </summary>
/// <remarks>
/// Passing the reference base straight through hands script the engine's internal sentinel for the
/// unresolvable state - a <see cref="JsString"/> reading <c>[[Unresolvable]]</c> - rather than
/// <c>undefined</c>, which is documented on <see cref="IReferenceResolver.TryUnresolvableReference"/> and
/// on <see cref="Reference.Base"/>. That is what scripts have always seen here, so it is kept and pinned
/// by a test; assigning <see cref="JsValue.Undefined"/> instead would be the tidier behaviour but a
/// breaking change for existing tenant scripts.
/// </remarks>
public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value)
{
value = reference.Base;

Loading…
Cancel
Save