From a751cab46a7489d4d766e24b5ee1eeed7f21c63d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 29 Jul 2026 14:15:39 +0300 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f --- .../Scripting/JintScriptEngine.cs | 2 +- .../Scripting/NullPropagation.cs | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 8ad87b9a6..428182cf7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -146,7 +146,7 @@ public sealed class JintScriptEngine(IMemoryCache cache, IOptions new CustomClrConverter(engine)); - engineOptions.SetReferencesResolver(NullPropagation.Instance); + engineOptions.SetReferencesResolver(NullPropagation.Instance, NullPropagation.Interests); engineOptions.Strict(); if (!Debugger.IsAttached) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs index ee7684dc6..8e8d0c724 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/NullPropagation.cs +++ b/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 { + /// + /// The situations this resolver actually answers, declared so the engine keeps the fast paths for + /// everything else. + /// + /// + /// Deliberately omitted are and + /// , 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. 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. + /// + public const ReferenceResolverInterests Interests = + ReferenceResolverInterests.NullishPropertyBase | + ReferenceResolverInterests.UnresolvableReference | + ReferenceResolverInterests.NonCallableCallee; + public static readonly NullPropagation Instance = new NullPropagation(); + /// + /// Answers a read of a name that resolves to no binding, so that an unknown name does not throw a + /// reference error. + /// + /// + /// Passing the reference base straight through hands script the engine's internal sentinel for the + /// unresolvable state - a reading [[Unresolvable]] - rather than + /// undefined, which is documented on and + /// on . That is what scripts have always seen here, so it is kept and pinned + /// by a test; assigning instead would be the tidier behaviour but a + /// breaking change for existing tenant scripts. + /// public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value) { value = reference.Base;