From 639dbd64542830ecae6e3650ef45828c34d0c788 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 4 Jul 2026 00:43:31 -0700 Subject: [PATCH 1/6] refactor(interpreter): extract realm state + statement handlers into partials (#1142) Pure mechanical partial-class split of the 2,898-line Interpreter.cs, no behaviour change: - New Interpreter.Realm.cs holds the realm/global-state cluster: the process- wide globals table (CreateGlobalsLookup + the ordering-sensitive Promise/ RegExp static trio, kept together and in textual order), the per-realm Symbol.for registry, Math, primitive prototypes, RegExp.prototype, and the globalThis routing helpers. - The statement dispatch core and all Visit* handlers move next to their Execute* bodies in Interpreter.Statements.cs. Interpreter.cs shrinks 2,898 -> 1,977 lines. dotnet test 15404/0. Part of #1094. --- Execution/Interpreter.Realm.cs | 373 +++++++++ Execution/Interpreter.Statements.cs | 749 ++++++++++++++++++ Execution/Interpreter.cs | 1093 +-------------------------- 3 files changed, 1125 insertions(+), 1090 deletions(-) create mode 100644 Execution/Interpreter.Realm.cs diff --git a/Execution/Interpreter.Realm.cs b/Execution/Interpreter.Realm.cs new file mode 100644 index 00000000..567393dc --- /dev/null +++ b/Execution/Interpreter.Realm.cs @@ -0,0 +1,373 @@ +using SharpTS.Modules; +using SharpTS.Parsing; +using SharpTS.Runtime; +using SharpTS.Runtime.BuiltIns; +using SharpTS.Runtime.Types; +using System.Collections.Frozen; + +namespace SharpTS.Execution; + +/// +/// Realm / global-state partial of : the process-wide +/// globals table (CreateGlobalsLookup) and the per-realm mutable +/// intrinsics that must not leak across realms or race across worker threads — +/// the Symbol.for registry, Math, the primitive prototypes, +/// RegExp.prototype, and globalThis routing. Extracted verbatim +/// from Interpreter.cs (#1142); no behaviour change. +/// +/// +/// The three ordering-sensitive static fields +/// (, +/// _globalConstants, ) +/// stay together and in textual order here: static field initializers run in +/// textual order within a single file, and CreateGlobalsLookup reads +/// the sentinel while RegExpConstructorObject reads the globals table. +/// +public partial class Interpreter +{ + /// + /// The %Promise% constructor sentinel registered when no Promise singleton + /// claimed the global first. MUST be declared before + /// — static initializers run in textual + /// order and CreateGlobalsLookup reads this field. + /// + internal static readonly SharpTSBuiltInConstructor PromiseConstructorSentinel = new( + BuiltInNames.Promise, + _ => throw new Exception("Runtime Error: Use 'new Promise(executor)' syntax.")); + + /// + /// Frozen dictionary of global constants and built-in singletons for fast lookup. + /// Combines global constants (NaN, Infinity, undefined) with built-in namespaces + /// (Math, JSON, Object, etc.) into a single lookup to minimize dictionary operations. + /// + private static readonly FrozenDictionary _globalConstants = CreateGlobalsLookup(); + + // The process-wide RegExp constructor singleton (a SharpTSBuiltInConstructor), + // resolved once from the static globals table. ECMA-262 §22.2.6.1 requires + // `RegExp.prototype.constructor === RegExp` and, by inheritance, + // `(/x/).constructor === RegExp` — both must reference this exact instance + // for strict-equality identity to hold. Cached so the regex property hot + // path returns it without a dictionary probe. Mirrors the compiled side, + // where the `$RegExp` Type token plays the same role. + internal static readonly object? RegExpConstructorObject = + _globalConstants.TryGetValue(BuiltInNames.RegExp, out var rxCtor) ? rxCtor : null; + + private static FrozenDictionary CreateGlobalsLookup() + { + var globals = new Dictionary + { + [BuiltInNames.NaN] = double.NaN, + [BuiltInNames.Infinity] = double.PositiveInfinity, + [BuiltInNames.Undefined] = Runtime.Types.SharpTSUndefined.Instance, + [BuiltInNames.Fetch] = Runtime.Types.SharpTSFetchGlobal.Instance, + + // SharedArrayBuffer constructor + [BuiltInNames.SharedArrayBuffer] = WorkerBuiltIns.SharedArrayBufferConstructor, + + // ArrayBuffer constructor + [BuiltInNames.ArrayBuffer] = WorkerBuiltIns.ArrayBufferConstructor, + + // DataView constructor + [BuiltInNames.DataView] = WorkerBuiltIns.DataViewConstructor, + }; + + // Add TypedArray constructors using centralized names + foreach (var typedArrayName in BuiltInNames.TypedArrayNames) + { + globals[typedArrayName] = WorkerBuiltIns.GetTypedArrayConstructor(typedArrayName); + } + + // Add Error constructors as global class variables + // This enables typeof Error, class MyError extends Error, const E = Error, etc. + var errorClass = new Runtime.Types.SharpTSErrorClass("Error", null); + globals[BuiltInNames.Error] = errorClass; + foreach (var errorTypeName in BuiltInNames.ErrorTypeNames) + { + if (errorTypeName != "Error") + globals[errorTypeName] = new Runtime.Types.SharpTSErrorClass(errorTypeName, errorClass); + } + + // Bare `Array` reference — needed for Array.prototype.X.apply() patterns + // that real-world CJS packages (yaml, lodash internals) rely on. + globals[BuiltInNames.Array] = Runtime.Types.SharpTSArrayGlobal.Instance; + + // Bare `Function` reference — required for `Function.prototype.call.bind(...)` + // patterns used by test262 propertyHelper.js (and many libraries' native- + // detection paths). Without this, the harness fails at load before any + // test body runs. + globals[BuiltInNames.Function] = Runtime.Types.SharpTSFunctionGlobal.Instance; + + // Node-style `global` alias for globalThis. CJS packages (lodash) + // detect the global object via `typeof global == 'object'` and alias + // its Array/Object/Date/etc. into a local scope. + var gtSingleton = BuiltInRegistry.Instance.GetSingleton(BuiltInNames.GlobalThis); + if (gtSingleton != null) + { + globals["global"] = gtSingleton; + } + + // Add built-in singletons (Math, JSON, Object, etc.) + // These are namespaces that resolve to singleton instances when accessed as variables + string[] singletonNames = + [ + BuiltInNames.Math, BuiltInNames.JSON, BuiltInNames.Object, + BuiltInNames.Number, BuiltInNames.String, BuiltInNames.Boolean, BuiltInNames.Symbol, + BuiltInNames.Console, BuiltInNames.Process, BuiltInNames.GlobalThis, + BuiltInNames.Reflect, BuiltInNames.Promise, BuiltInNames.Atomics, + "Buffer", + // WebCrypto global (#1063): bare `crypto` — an import binding shadows it. + "crypto", + ]; + foreach (var name in singletonNames) + { + var singleton = BuiltInRegistry.Instance.GetSingleton(name); + if (singleton != null) + { + globals[name] = singleton; + } + } + + // Add built-in constructors as global variables (Map, Set, Date, RegExp, etc.) + // Enables typeof Map, val instanceof Map, passing Map as value, Map.groupBy(), etc. + foreach (var (name, factory) in BuiltInConstructorFactory.GetConstructors()) + { + if (!globals.ContainsKey(name)) + globals[name] = new SharpTSBuiltInConstructor(name, factory); + } + + // Expose global functions (parseFloat, parseInt, isNaN, isFinite, + // structuredClone, setTimeout/clearTimeout, etc.) as first-class + // callable values so they can be referenced by name — not just + // invoked directly. CommonJS packages (lodash) alias `var + // freeParseFloat = parseFloat`, and user code may do + // `typeof parseFloat === 'function'`. + string[] globalFunctionNames = + [ + BuiltInNames.ParseInt, BuiltInNames.ParseFloat, + BuiltInNames.IsNaN, BuiltInNames.IsFinite, + BuiltInNames.StructuredClone, + BuiltInNames.EncodeURIComponent, BuiltInNames.DecodeURIComponent, + BuiltInNames.SetTimeout, BuiltInNames.ClearTimeout, + BuiltInNames.SetInterval, BuiltInNames.ClearInterval, + BuiltInNames.QueueMicrotask, + ]; + foreach (var name in globalFunctionNames) + { + if (!globals.ContainsKey(name)) + globals[name] = new SharpTSGlobalFunction(name); + } + + // Bind value-position globals for built-ins that were previously only + // reachable through special-cased `new` expressions or member access + // (#208): bare `AbortSignal`/`Intl`/`ReadableStream`/... otherwise + // throw "Undefined variable". + // + // AbortSignal and Intl are namespace-style globals: member access on + // SharpTSBuiltInConstructor routes through the namespace registry + // (AbortSignal.abort/timeout/any, Intl.NumberFormat/...), while + // direct construction throws per spec (AbortSignal has no public + // constructor; Intl is not a constructor). + globals["AbortSignal"] = new SharpTSBuiltInConstructor("AbortSignal", + _ => throw new Exception("Runtime Error: TypeError: AbortSignal cannot be constructed directly. Use AbortSignal.abort(), AbortSignal.timeout(), or AbortController.")); + globals["Intl"] = new SharpTSBuiltInConstructor("Intl", + _ => throw new Exception("Runtime Error: TypeError: Intl is not a constructor.")); + + // Web-streams constructors: the same singletons stream/web exports, + // so `new ReadableStream(...)`, `ReadableStream.from(...)`, and + // value-position references all share one identity. + globals[BuiltInNames.ReadableStream] = Runtime.Types.SharpTSReadableStreamConstructor.Instance; + globals[BuiltInNames.WritableStream] = Runtime.Types.SharpTSWritableStreamConstructor.Instance; + globals[BuiltInNames.TransformStream] = Runtime.Types.SharpTSTransformStreamConstructor.Instance; + + // MessageChannel as a value (construction already worked by name). + globals[BuiltInNames.MessageChannel] = WorkerBuiltIns.MessageChannelConstructor; + + // Symbol as a value-position global (#234): `typeof Symbol`, + // `const f = Symbol`, and `(Symbol as any).species` need a real + // binding. Its namespace is registered as non-singleton (member + // access routes through SymbolBuiltIns via GetMember), so the + // singleton loop above didn't bind it. The factory implements the + // call form Symbol(description); JS has no `new Symbol()`. + globals[BuiltInNames.Symbol] = new SharpTSBuiltInConstructor( + BuiltInNames.Symbol, + args => new SharpTSSymbol(args.Count > 0 && args[0] is not SharpTSUndefined + ? args[0]?.ToString() + : null)); + + // Promise needs a bare-reference global so `x instanceof Promise`, + // `typeof Promise === 'function'`, and stdlib modules that carry + // Promise as a value can type-check/run. Its namespace is registered + // as non-singleton (to preserve special `new Promise(executor)` + // handling), so it wasn't picked up by the loops above. Register a + // minimal constructor sentinel — `new Promise(executor)` has its + // own dedicated path and does not route through this factory. + if (!globals.ContainsKey(BuiltInNames.Promise)) + { + globals[BuiltInNames.Promise] = PromiseConstructorSentinel; + } + + return globals.ToFrozenDictionary(); + } + + /// + /// The value bare Promise resolves to — whatever the global table + /// actually holds (a registry singleton when one exists, otherwise + /// ). Surfaced as + /// promise.constructor by PromiseBuiltIns.GetMember so the + /// ECMA-262 §27.2.5.1 identity holds: + /// Promise.resolve(1).constructor === Promise (#221). + /// + internal static object PromiseGlobalValue => _globalConstants[BuiltInNames.Promise]; + + // Per-realm RegExp.prototype. Held on the Interpreter (not on the + // process-wide SharpTSBuiltInConstructor singleton) so user mutations + // — `delete RegExp.prototype[Symbol.split]`, `Object.defineProperty`, + // etc. — stay scoped to this realm. Lazily populated on first read of + // `RegExp.prototype`. + private Runtime.Types.SharpTSObject? _regExpPrototype; + internal Runtime.Types.SharpTSObject GetRegExpPrototype() + => _regExpPrototype ??= Runtime.BuiltIns.RegExpBuiltIns.BuildPrototype(); + + // Per-realm Symbol.for registry. Held on the Interpreter (not as a + // process-wide static on SharpTSSymbol) so `Symbol.for(k)` returns a + // symbol unique to this realm and `Symbol.keyFor` cannot leak + // registrations across Interpreter instances. Each realm is its own agent + // per ECMA-262, so a separate registry is the correct semantics — and it + // removes a cross-thread data race: the old static was a plain Dictionary + // mutated by every realm in the process, including concurrent worker + // threads. Mirrors the per-realm RegExp.prototype (#101). Well-known + // symbols (Symbol.iterator, …) are NOT in this registry; they remain + // process-wide singletons. Lazily allocated; thread-confined to this + // realm's execution thread, so no lock is needed. + private Dictionary? _symbolRegistry; + private Dictionary? _symbolReverseRegistry; + + /// + /// Returns this realm's registered symbol for , + /// creating and registering one on first use (ECMA-262 Symbol.for). + /// + internal Runtime.Types.SharpTSSymbol SymbolFor(string key) + { + _symbolRegistry ??= []; + if (_symbolRegistry.TryGetValue(key, out var existing)) + return existing; + + var symbol = new Runtime.Types.SharpTSSymbol(key); + _symbolRegistry[key] = symbol; + (_symbolReverseRegistry ??= [])[symbol] = key; + return symbol; + } + + /// + /// Returns the registry key for in this realm, or + /// null if it was not produced by this realm's Symbol.for + /// (ECMA-262 Symbol.keyFor). + /// + internal string? SymbolKeyFor(Runtime.Types.SharpTSSymbol symbol) + => _symbolReverseRegistry is not null + && _symbolReverseRegistry.TryGetValue(symbol, out var key) + ? key + : null; + + // Per-realm Math. Math is an extensible ECMA-262 object: guest code may add + // properties (`Math.x = 1`), which must not leak across realms or race + // across worker threads. Held per-Interpreter, mirroring RegExp.prototype + // (#101). The base members (PI, sqrt, …) are stateless and resolved the + // same way for every instance; only the per-instance `_extras` overlay + // differs. Within a realm both the bare `Math` global and `globalThis.Math` + // resolve to this one instance, so `Math === globalThis.Math` holds. + private Runtime.Types.SharpTSMath? _math; + internal Runtime.Types.SharpTSMath GetMath() => _math ??= new Runtime.Types.SharpTSMath(); + + /// + /// Resolves a per-realm mutable built-in intrinsic by its global name + /// (currently Math). These are the built-ins moved off process-global + /// singletons so a realm's guest mutations stay realm-local. Returns + /// false for every other name, leaving normal global resolution + /// unchanged. + /// + internal bool TryGetRealmIntrinsic(string name, out object? value) + { + if (IsRealmIntrinsicName(name)) + { + value = GetMath(); + return true; + } + value = null; + return false; + } + + /// + /// Names of the per-realm mutable built-ins resolved off the Interpreter + /// rather than the shared global-constants table or the namespace + /// fast-path. Used to keep all resolution routes (bare global, namespace + /// member access, globalThis) pointing at the one realm instance so + /// method identity holds (Math.max === Math.max). + /// + internal static bool IsRealmIntrinsicName(string name) => name == "Math"; + + // Per-realm String/Number/Boolean.prototype. Each is an extensible ECMA-262 + // object carrying a guest-writable _extras bag, so — like Math and + // RegExp.prototype (#101) — it is held per-Interpreter: guest writes + // (`String.prototype.x = …`, indexed/`length` assignments Test262 makes + // before calling Array.prototype.* on a primitive) stay realm-local and + // don't race across worker threads. The namespace objects + // (String/Number/Boolean themselves) are immutable and stay shared + // singletons; only the mutable prototypes are per-realm. + private Runtime.Types.SharpTSStringPrototype? _stringPrototype; + private Runtime.Types.SharpTSNumberPrototype? _numberPrototype; + private Runtime.Types.SharpTSBooleanPrototype? _booleanPrototype; + internal Runtime.Types.SharpTSStringPrototype GetStringPrototype() => _stringPrototype ??= new(); + internal Runtime.Types.SharpTSNumberPrototype GetNumberPrototype() => _numberPrototype ??= new(); + internal Runtime.Types.SharpTSBooleanPrototype GetBooleanPrototype() => _booleanPrototype ??= new(); + + // Per-realm globalThis. The global object holds guest-assigned properties + // (`globalThis.x = …`), which must stay realm-local and not race across + // worker threads, so each Interpreter owns its own — like RegExp.prototype + // (#101), Math, and the primitive prototypes. Built-in namespaces (Math, + // JSON, …) are still resolved live through the shared BuiltInRegistry, so + // `globalThis.JSON === JSON` and `globalThis.Math === Math` still hold; only + // the user-property bag is per-realm. Bare `globalThis` and the Node + // `global` alias resolve here (see LookupVariableRV), and sloppy-mode `this` + // binds to it. + private Runtime.Types.SharpTSGlobalThis? _globalThis; + internal Runtime.Types.SharpTSGlobalThis GlobalThis => _globalThis ??= new Runtime.Types.SharpTSGlobalThis(); + + /// + /// Resolves String/Number/Boolean.prototype to + /// this realm's prototype instance when is the + /// corresponding built-in namespace, so the read is realm-local rather than + /// the shared singleton. Returns false for any other receiver, + /// leaving normal member resolution unchanged. + /// + private bool TryGetRealmPrototypeForNamespace(object? obj, out object? prototype) + { + switch (obj) + { + case Runtime.Types.SharpTSStringNamespace: + prototype = GetStringPrototype(); + return true; + case Runtime.Types.SharpTSNumberNamespace: + prototype = GetNumberPrototype(); + return true; + case Runtime.Types.SharpTSBooleanNamespace: + prototype = GetBooleanPrototype(); + return true; + default: + prototype = null; + return false; + } + } + + /// + /// Reads a property off globalThis honoring per-realm intrinsics: a + /// guest own-assignment (globalThis.Math = x) wins, then the realm + /// intrinsic (so globalThis.Math === Math within a realm), then the + /// normal built-in/global resolution. Behaviour is identical to + /// globalThis.GetProperty for every non-intrinsic name. + /// + private object? ResolveGlobalThisRead(Runtime.Types.SharpTSGlobalThis globalThis, string key) + => !globalThis.HasUserProperty(key) && TryGetRealmIntrinsic(key, out var intrinsic) + ? intrinsic + : globalThis.GetProperty(key); +} diff --git a/Execution/Interpreter.Statements.cs b/Execution/Interpreter.Statements.cs index 72077be3..1d424bff 100644 --- a/Execution/Interpreter.Statements.cs +++ b/Execution/Interpreter.Statements.cs @@ -1,10 +1,17 @@ +using SharpTS.Modules; +using SharpTS.Modules.Stdlib; using SharpTS.Parsing; +using SharpTS.Parsing.Visitors; using SharpTS.Runtime; using SharpTS.Runtime.BuiltIns; using SharpTS.Runtime.BuiltIns.Modules; +using SharpTS.Runtime.BuiltIns.Modules.Interpreter; +using SharpTS.Runtime.DotNet; using SharpTS.Runtime.Exceptions; using SharpTS.Runtime.Types; using SharpTS.TypeSystem; +using System.Collections.Frozen; +using System.Threading; namespace SharpTS.Execution; @@ -1654,4 +1661,746 @@ private void DisposeResource(object? resource, bool isAsync) // For other types, return null (no symbol property access) return null; } + + /// + /// Internal wrapper for Execute that allows evaluation contexts to dispatch statements. + /// + /// The statement to execute. + /// The execution result. + internal ExecutionResult ExecuteStatement(Stmt stmt) => Execute(stmt); + + /// + /// Internal async wrapper for statement execution using registry-based dispatch. + /// Uses DispatchStmtAsync which falls back to sync handlers when no async handler exists. + /// + /// The statement to execute. + /// A task containing the execution result. + internal async Task ExecuteStatementAsync(Stmt stmt) + { + return await _registry.DispatchStmtAsync(stmt, this); + } + + /// + /// Dispatches a statement to the appropriate execution handler using the registry. + /// + /// The statement AST node to execute. + /// + /// Handles all statement types including control flow (if, while, for, switch), + /// declarations (var, function, class, enum), and control transfer (return, break, continue, throw). + /// Control flow uses for non-local jumps. + /// + private ExecutionResult Execute(Stmt stmt) + { + return _registry.DispatchStmt(stmt, this); + } + + // Statement handlers - called by the registry + + internal ExecutionResult VisitBlock(Stmt.Block block) => + ExecuteBlock(block.Statements, new RuntimeEnvironment(_environment)); + + internal ExecutionResult VisitLabeledStatement(Stmt.LabeledStatement labeledStmt) => + ExecuteLabeledStatement(labeledStmt); + + internal ExecutionResult VisitSequence(Stmt.Sequence seq) + { + // Execute in current scope (no new environment) + foreach (var s in seq.Statements) + { + var result = Execute(s); + if (result.IsAbrupt) return result; + } + return ExecutionResult.Success(); + } + + internal ExecutionResult VisitExpression(Stmt.Expression exprStmt) + { + Evaluate(exprStmt.Expr); + return ExecutionResult.Success(); + } + + internal ExecutionResult VisitIf(Stmt.If ifStmt) + { + if (IsTruthy(Evaluate(ifStmt.Condition))) + { + return Execute(ifStmt.ThenBranch); + } + else if (ifStmt.ElseBranch != null) + { + return Execute(ifStmt.ElseBranch); + } + return ExecutionResult.Success(); + } + + internal ExecutionResult VisitWhile(Stmt.While whileStmt) + { + var labels = TakePendingLoopLabels(); + return ExecuteWhileCore(_syncContext, whileStmt, labels).GetAwaiter().GetResult(); + } + + internal ExecutionResult VisitDoWhile(Stmt.DoWhile doWhileStmt) + { + var labels = TakePendingLoopLabels(); + return ExecuteDoWhileCore(_syncContext, doWhileStmt, labels).GetAwaiter().GetResult(); + } + + internal ExecutionResult VisitFor(Stmt.For forStmt) + { + // Drain labels parked by an enclosing labeled statement before running the initializer, + // so `continue