diff --git a/docs/plans/generic-constructor-compatibility.md b/docs/plans/generic-constructor-compatibility.md index 757523a34..fa227e3b0 100644 --- a/docs/plans/generic-constructor-compatibility.md +++ b/docs/plans/generic-constructor-compatibility.md @@ -101,3 +101,27 @@ Stop after this declared distinct-generic-constructor relation passes the pinned boundary and controls. Repeated class evaluation/computed keys remain #1906; arbitrary dynamic superclass support remains #1907. General structural class, conditional-type or inference conformance is outside this successor. + +## Implementation evidence (#1963) + +The constructor relation now projects constructor parameters, constructed instance +members and required static members into a single contextual signature relation. +Alpha-renaming and inference therefore use one substitution across all three. +Private/protected declaration origins and ECMAScript private brands are checked +before structural projection, including separate static visibility metadata and +common-base origins. Existing declaration-identity fast paths and cache keys remain. +Class expressions now publish their private field/method metadata as private metadata. + +Runtime construction reads generic constructor bindings rather than selecting the +original expression template after reassignment. Literal inferred type arguments +are widened to their corresponding CLR primitive when closing an aliased constructor. + +On Windows ARM64/.NET 10.0.12, all three retained positive sources match Node in CLI +interpretation and IL-verified standalone output. Their shared tests also exercise +the interpreter, in-process compiled execution and serialized verification/execution +with 30-second harness deadlines. All four retained negatives remain rejected. +The final focused selection passes 52 tests; the broader generic/compatibility/class +expression/private-field selection passes 936 tests with compiled IL verification. +Release, code-quality gates and the actual analyzer-aware AOT/trim/single-file build +pass; its zero-warning inventory matches the committed baseline. Hosted execution +was not exercised by these new tests. diff --git a/docs/plans/namespace-class-identity.md b/docs/plans/namespace-class-identity.md index f42968bb4..f551eb3b4 100644 --- a/docs/plans/namespace-class-identity.md +++ b/docs/plans/namespace-class-identity.md @@ -89,3 +89,25 @@ evidence remains missing; the new controls demonstrate the same diagnostic famil without claiming to reconstruct the historical source. The finite successor is [#1968](https://github.com/nickna/SharpTS/issues/1968). + +## Implementation verification (#1968) + +The checked declaration now supplies the canonical emitted class owner. Namespace +and module scopes are retained through method, constructor and property-dispatch +emission, and checked references resolve that owner. Namespace object and variable +storage is module-local. Interpreter exports accept namespace declarations, and +reopened namespaces retain earlier exported class bindings. Emitted class metadata +keeps the guest simple name independently of the canonical CLR name. + +Both sibling fixtures now print `1 2 9`; both module import orders print `1 2`. +These unchanged fixtures pass API execution in both engines and CLI compilation, +IL verification and standalone execution, with empty stderr and a 30-second +per-case deadline. Additional shared controls cover nested/reopened namespaces, +private storage/origins, typed signatures, inheritance, bare self-construction, +module namespace variables and constructor aliases with observable names. + +The affected namespace/class-expression/generic/private/module selection passes +711 tests with `SHARPTS_VERIFY_COMPILED=1`, including #1781's original standalone +construction regression. The quality gate passes. Release and actual AOT analyzer +baseline checks pass; the analyzer inventory has zero warnings. Hosted execution is not claimed +for the new fixtures. The historical baseline evidence above remains unchanged. diff --git a/docs/plans/private-in-brand-checks.md b/docs/plans/private-in-brand-checks.md index 5feed60d5..00f756abd 100644 --- a/docs/plans/private-in-brand-checks.md +++ b/docs/plans/private-in-brand-checks.md @@ -94,3 +94,68 @@ Stop after the specified grammar, declared-name lookup and brand cases pass. Private-method values remain #1961; repeated class-evaluation identity and computed-key behavior remain #1906. General parser compatibility, broader class features and unrelated conformance changes do not enter this successor. + +## Implementation verification (#1962) + +The implementation adds the relational AST node, lexical checked-owner lookup, +right-operand traversal and presence queries in both engines. Nested class +expressions retain their enclosing evaluation owner; derived instances use the +declaring template's brand, and generic static probes use the exact guest owner. +Instance fields install in source order after their initializer completes, while +the method brand is available before those initializers run. + +Validation on Windows ARM64, 2026-10-06: + +| Check | Recorded result | +| --- | --- | +| Release build of `SharpTS.Tests` | Succeeded, zero errors; existing NU1902 package advisory | +| `PrivateInTests`, `PrivateInParserTests`, `AstDispatchTests` | 95 passed, zero failed | +| Private/generic/class-expression/arrow/repeated-class selection | 2,043 passed, zero failed, after the #1965 capture follow-up | +| Runtime reference fixtures | All 14 matched Node v25.5.0 stdout, exit 0 and empty stderr | +| Code-quality gate | Passed: 27 duplicate groups, zero errors | +| Actual AOT/trim/single-file analyzer restore and rebuild | Passed: unchanged zero-warning baseline | +| Pinned TypeScript checker smoke gate | Passed: all 32 corpus cases, clean TypeScript 6.0.3 checkout | + +The shared runtime tests execute both engines, serialize and verify emitted IL, +and execute the resulting assembly. Thirteen fixtures also execute without a +deployed `SharpTS.dll`; the proxy fixture uses the deployed runtime. Module-owner +controls run in both engines and verify the compiled multi-file assembly. The +default execution deadline is 30 seconds. Runtime reference sources were emitted +with TypeScript 7.0.2 (`--target ES2022 --skipLibCheck --noImplicitAny false`); +the final flag permits the timing fixtures' unannotated private fields without +changing their runtime source. Node's native type stripper was not used as the +reference parser for the generator fixtures. + +Two added static-write controls reject assignment before a private field's +initializer has completed, leave its brand absent, and evaluate the RHS once. +They also cover an abrupt RHS and successful assignment after installation for +a generic named owner and repeated class expressions. The preserved pre-fix +compiler (`28DF7ED461F26E6AEE0B34B29FA287925CC2C4D1FC9DB30298A13D5768E621E3`) +failed these semantic controls in both engines even though its emitted IL +verified. The candidate matches Node; the two reference emissions additionally +set `--useUnknownInCatchVariables false` for the caught-error message assertion. + +The pinned Test262 comparison selected all 19 `class-fields-private-in` files at +revision `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`. In each engine the unchanged, +preserved `c782c870` binary reported 11 parse errors and eight deferred negative +cases; the candidate reported eight passes, three parse errors and the same +eight deferred negatives. All worker processes exited 0 with empty stderr and +no runtime failures or timeouts. Two remaining errors require private accessor +declaration parsing; the third uses `function await()` as an ordinary identifier. +The deferred negative cases are not credited as validated semantics; local +parser/checker negative tests provide the separate negative coverage. + +Evidence is retained under `artifacts/issue1962-lexical-*`, +`artifacts/issue1962-static-write/` (focused TRX, reference and pre-fix outputs), +`artifacts/issue1965-shadow-capture/broader-tests.log`, +`artifacts/validation/code-quality-final.log` and `artifacts/validation/issue1962/`. +The latter includes exact commands, binary hashes and every Test262 classification. +The final candidate rerun preserves the same eight passes, three parse errors and +eight deferred negatives in each engine. A separate 28-process CLI matrix checks +the three retained positives against Node, interpretation, verified standalone +emission, and hosted initialization with default declarations and `--noLib`. +Hosted execution uses the public factory and `InitializeAsync`, with a 30-second +process deadline; it is separate from ordinary entry-point execution. These +checks do not claim native AOT publication. The consolidated PR records the +full-suite results separately. The checker gate uses the clean pinned TypeScript +6.0.3 corpus and its committed diagnostics without live `tsc` or npm. diff --git a/docs/plans/private-method-values.md b/docs/plans/private-method-values.md index 5da7dc61a..ffa0a8ac4 100644 --- a/docs/plans/private-method-values.md +++ b/docs/plans/private-method-values.md @@ -1,5 +1,53 @@ # Private-method values: bounded implementation design (#1901) +## Implementation evidence (#1961) + +Private method extraction now returns a canonical, unbound callable after checking +the receiver against the lexical declaration's brand. Ordinary declarations cache +their callable in a non-generic adapter; generic instance access uses the existing +private-instance bridge. Class expressions cache private callables and static +private state on each evaluated class definition. Repeated expression evaluations +therefore retain different brands, values, and captured environments. + +The adapter executes the source body with an explicit guest receiver. Public +property reads, writes, calls, and returned closures use that receiver. Private +access continues to use the lexical owner's storage, including after suspension. +Name, arity, defaults/rest, strict receiver semantics, and non-constructibility use +the emitted function contracts. `PrivateClassElementRegistry` checks adapter, +cache, constructor, and initializer ownership and requires their emitted bodies +before completion. No runtime type generation or SharpTS deployment dependency +is introduced. + +`PrivateMethodValueTests` executes eleven sources in interpretation, in-process +compilation, verified saved IL, and standalone deployment. The sources cover +generic identity, extraction evaluation once, borrowed receivers, public writes +and calls, private brands, static state, all four method kinds, returned closures, +defaults/rest, and a timer suspension. Separate tests cover same-named module +declarations and checker rejection of outside access, assignment, and undeclared +private names. Node v25.5.0 matches all eleven expected outputs; TypeScript 7.0.2 +accepts the two retained positive fixtures and rejects the outside-access control. + +A targeted Test262 comparison against unchanged `c782c870`, at the pinned corpus +revision `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`, improves +`private-method-get-and-call.js` from RuntimeError to Pass in both engines, with +no regressions in the eight selected files. Three metadata files retain their +parser errors (method-trailing semicolons), and three compiled inherited/inner +arrow controls retain their runtime errors. These outcomes are not counted as +passing conformance checks or silently removed from the comparison. + +Final verification passes 22 focused tests and 779 affected regressions with +strict compiled IL verification enabled. The retained positive CLI fixtures also +pass saved-assembly IL verification and exact-output standalone execution without +SharpTS.dll. The Release solution build and all code-quality gates pass; the +analyzer-aware restore/rebuild reports zero AOT/trim/single-file warnings and +matches the checked baseline. The bounded regression selection excludes the +pre-existing lock-decorator IL failure reproduced at `69f2b081`; the separate +async-arrow/inheritance IL failures recorded for #1965 remain unchanged. + +The remaining runtime-valued local declaration work is tracked separately by +#1967; this evidence does not claim that repeatedly executed local declarations +already receive the class-expression evaluation identity. + ## Evidence and disposition The notes linked by #1901 and #1854 report preserved generic and non-generic diff --git a/docs/plans/repeated-class-evaluation.md b/docs/plans/repeated-class-evaluation.md index bffc375fe..70b03835c 100644 --- a/docs/plans/repeated-class-evaluation.md +++ b/docs/plans/repeated-class-evaluation.md @@ -97,3 +97,94 @@ new syntax and unrelated conformance failures remain their existing tasks. The inline prototype-call IL failure is a separate verification defect, [#1966](https://github.com/nickna/SharpTS/issues/1966); its failure is retained rather than used to claim this representation work is repaired. + +## Implementation evidence for #1964 + +Each class-expression evaluation now returns a fresh emitted `$ClassDefinition` +with its own prototype, constructor back-reference, name and arity. A generated +factory passes the selected definition into the shared template's constructor +before instance field initialization. Generic arguments are erased at that +factory boundary, so instances of one guest definition share its prototype. +Runtime construction, Reflect construction, `typeof`, and `instanceof` recognize +these definition values. The interpreter uses definition identity for class +brands and evaluates instance initializers with their owning environment and +receiver. Named expressions retain their local body binding. + +`RepeatedClassDefinitionTests` covers repeated evaluations in functions, loops, +closures, modules, async and generator suspension, plus generic arguments, +initializers, prototype mutation, default arguments and bound construction. The +ten source controls run in both engines and as serialized, IL-verified programs. +The module control runs in both engines. A reused runtime emitter test separately +verifies fresh metadata ownership and saved IL in ordinary and hosted emission; +this is emission evidence, not a hosted class-factory execution claim. + +On Windows ARM64/.NET 10.0.12 and Node v25.5.0, the unchanged +`constructors-only.ts` now prints `false` in verified CLI standalone output. +`prototype-separate-values.ts` prints `false`, `false`, `true true`, and +`true false`, matching Node. Both standalone runs complete within 30 seconds +with empty stderr and without SharpTS.dll in the output directory. The inline +`identity.ts` source remains the separate #1966 verifier control. + +The affected class-expression/owner/local-class, constructor, generic, computed +member, namespace and prototype suite passes all 1,181 tests with compiled IL +verification enabled. The full Release solution build and code-quality gates +pass (28 duplicate groups, zero errors). The actual AOT/trim/single-file analyzer +passes the unchanged zero-warning baseline. Definition-owned computed keys, +capture snapshots and static initialization continue in #1965. + +## Implementation evidence for #1966 + +The unchanged inline `identity.ts` reproduced the reported `BackwardBranch` +error at offset 709 after #1964. The reflection constructor's argument-padding +loop jumped forward over its body and then backward into it while the enclosing +prototype query retained values on the evaluation stack. Moving the loop check +to the top makes its stack height determinable in one forward scan. + +The original source now passes CLI IL verification and prints `false` / `false` +in standalone output, matching Node, within 30 seconds with empty stderr. +`InlineConstructorPrototypeTests` verifies all three source controls as saved +assemblies and executes them in both engines and without SharpTS.dll. Additional +controls retain left-to-right, exactly-once callee/argument evaluation, omitted +parameter defaults, and prototype identity for an aliased CLR constructor. +All 279 affected constructor, class-expression owner, generic-constructor and +prototype tests pass with compiled verification enabled; the Release core/test +build passes. The original reference expectation and verifier remain intact. + +## Implementation evidence for #1965 + +Each class-expression definition now owns its ordered computed keys, captured +bindings, member descriptors, static fields and initialization. Keys are evaluated +once, before static fields/blocks, and retained across suspension; instances read +their lexical definition's keys rather than mutable template static fields. +String and Symbol methods/accessors register in source order on the fresh +prototype or constructor. Static methods retain the selected definition separately +from the caller's `this`. Captured bindings retain their original display-class or +cell storage, so later writes and closures returned by methods remain live. +Async/generator method state machines retain the instance or static definition, +and suspended closure emitters share the same environment population path. +Instance method values use pre-emitted adapters that retain the lexical definition +separately from their explicit guest receiver. Borrowing a method across class +evaluations therefore retains its original captures while observing the caller's +public fields, including after suspension. + +`RepeatedClassStateTests` exercises 18 source controls in interpretation, +in-process compilation, serialized IL verification/execution and standalone +deployment, plus two module execution modes with serialized module verification. +The source controls include retained constructors after a second evaluation, +string/Symbol fields and members, accessors, static captures and borrowed +receivers, initialization order, abrupt keys, interleaved awaits, inherited keys, +awaited keys/heritage, live captured writes and closures returned after suspension. +All 18 match Node v25.5.0. The unchanged `computed-keys.ts` passes verified CLI +standalone execution within 30 seconds, with empty stderr and no SharpTS.dll, +printing `false 2` / `5 5 undefined undefined 5`. The unchanged one-definition +generic computed-key expectation remains in the affected regression selection. + +The full Release solution build passes. Quality gates report 27 duplicate groups +and zero errors; the obsolete suspended-closure duplicate exception was removed +after sharing emission. The actual AOT/trim/single-file analyzer matches the +unchanged zero-warning baseline. The affected verification selection has 1,596 +passing tests. A separately recorded async-lock verifier failure also reproduces +on unchanged `69f2b081`; three other failures in the wider async selection likewise +reproduce there. These baseline failures are not counted as passes. Hosted +execution is not part of this evidence. Runtime-valued ordinary-user-class +heritage remains the separate #1967 task. diff --git a/docs/plans/runtime-valued-superclass.md b/docs/plans/runtime-valued-superclass.md index 962346419..534a4579d 100644 --- a/docs/plans/runtime-valued-superclass.md +++ b/docs/plans/runtime-valued-superclass.md @@ -35,6 +35,42 @@ in-process harness. No behavior repair is claimed by this investigation. The finite implementation successor is [#1967](https://github.com/nickna/SharpTS/issues/1967), dependent on #1964. +## Implementation successor (#1967) + +The original failing outputs above remain investigation evidence. The bounded +repair retains each evaluated parent in #1964's guest class definition and uses +explicit-receiver entries on emitted user templates for initialization and +ordinary methods. The child constructor and instance prototype link to the +selected guest parent; inherited instance/static lookup and `instanceof` follow +those links. A runtime child allocates one receiver and initializes its parent on +that receiver. Checked known-parent declarations keep their CLR inheritance path. + +Runtime validation requires an emitted ordinary-user-class entry before static +initialization. The adapter boundary excludes generic, private, computed, +accessor, suspended-method, destructured/parameter-property and constructor-return +replacement shapes; their existing known-parent paths are retained. Arbitrary +host/CLR parents, exotic constructors and proxy construction are not added. +Constructor defaults, rest arguments and the caller's `arguments` list are +preserved for the supported ordinary path. + +On 2026-10-06, `RuntimeSuperclassTests`' 13 cases pass: the three investigation +fixtures, alternating Left/Right selections with real pending suspension at both +an expression and local declaration, forwarding, original receiver/`super` +behavior, expression-parent identity, defaults/rest/arguments/static receiver, +and invalid/rejected heritage before static initialization. Runtime execution and +serialized IL verification are asserted separately. The fixtures also pass CLI +standalone and hosted execution with both default declarations and `--noLib`; +these deployment builds use `--verify` and omit a `SharpTS.dll` payload. For the +deployment controls only, the already-called `main` runner is renamed to avoid +the EXE/hosted automatic entry-point convention calling it twice. + +The focused Release run passes 169 tests across runtime superclass, arguments, +async local declarations, expression owner identity, repeated class state and +namespace class ownership. This includes the retained known-parent controls and +generic/computed identity controls. The Release test-project build succeeds with +the existing NU1902 package warning. Consolidated quality/AOT gates are recorded +with the issues 1961–1968 PR; this section does not claim Native AOT publication. + ## Representation decision **Store and use the evaluated superclass in the guest class definition, using diff --git a/eng/CodeQuality/duplicates.json b/eng/CodeQuality/duplicates.json index e3264a5ed..bf58776a7 100644 --- a/eng/CodeQuality/duplicates.json +++ b/eng/CodeQuality/duplicates.json @@ -175,14 +175,6 @@ ], "Reason": "Net and TLS servers expose matching names but bind methods to different transport implementations; retain the explicit transport adapters." }, - { - "Hash": "b9c0a74f210d34929340b991ac7822719362ccb3f317c74c54af1aa6c9730260", - "Members": [ - "src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs::AsyncGeneratorMoveNextEmitter.EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass)", - "src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs::AsyncMoveNextEmitter.EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass)" - ], - "Reason": "Async and async-generator emitters own different state machines; retain matching closure threading until a shared capture-emission contract is extracted." - }, { "Hash": "d53d7b94262ab121d5902bfd0de8b29cc05e24ede4a7a03b176c758ae601a880", "Members": [ diff --git a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs index 770669a79..6f2fae48f 100644 --- a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs @@ -337,87 +337,9 @@ private void EmitAsyncArrowFunction(Expr.ArrowFunction af) private void EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass) { - if (_ctx!.DisplayClassConstructors == null || !_ctx.DisplayClassConstructors.TryGetValue(af, out var displayCtor)) - { - _il.Emit(OpCodes.Ldnull); - SetStackUnknown(); - return; - } - - _il.Emit(OpCodes.Newobj, displayCtor); - - // Thread the entry-point display class into the arrow's $entryPointDC field so it reads - // captured TOP-LEVEL variables through shared storage (the async-generator analog of #732). - if (_ctx.ArrowEntryPointDCFields?.TryGetValue(af, out var entryPointDCField) == true && - _ctx.EntryPointDisplayClassStaticField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldsfld, _ctx.EntryPointDisplayClassStaticField); - _il.Emit(OpCodes.Stfld, entryPointDCField); - } - - // Thread the state machine's function display class into the arrow's $functionDC field so a - // write to a captured-and-mutated generator local reaches shared storage instead of a by-value - // snapshot — the case the compile-time guard previously rejected (#725). - if (_ctx.ArrowFunctionDCFields?.TryGetValue(af, out var functionDCField) == true && - _builder.FunctionDCField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, _builder.FunctionDCField); - _il.Emit(OpCodes.Stfld, functionDCField); - } - - if (_ctx.DisplayClassFields == null || !_ctx.DisplayClassFields.TryGetValue(af, out var fieldMap)) - { - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); - return; - } - - // Populate captured fields - foreach (var (capturedVar, field) in fieldMap) - { - _il.Emit(OpCodes.Dup); - - // Per-iteration cell capture (#650): snapshot the StrongBox REFERENCE. - if (_ctx.CellBindingLocals.TryGetValue(capturedVar, out var cellLocal)) - { - _il.Emit(OpCodes.Ldloc, cellLocal); - _il.Emit(OpCodes.Stfld, field); - continue; - } - - // #767: pivot a captured nested-block shadow to its renamed storage (identity otherwise). - var sourceVar = PivotCaptureSource(_analysis.BlockScopeCaptureRenames, af, capturedVar); - - var hoistedField = _builder.GetVariableField(sourceVar); - if (hoistedField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, hoistedField); - } - else if (capturedVar == "this" && _builder.ThisField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, _builder.ThisField); - } - else if (_ctx.Locals.TryGetLocal(sourceVar, out var local)) - { - _il.Emit(OpCodes.Ldloc, local); - } - else if (!TryEmitGlobalVariable(sourceVar)) - { - _il.Emit(OpCodes.Ldnull); - } - - _il.Emit(OpCodes.Stfld, field); - } - - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); + if (_ctx!.DisplayClassConstructors.TryGetValue(af, out var constructor)) + EmitCapturingArrowViaHooks(af, method, constructor); + else EmitNullConstant(); } private void EmitNonCapturingArrowFunction(MethodBuilder method) diff --git a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs index 9cef3ebee..a26b7358b 100644 --- a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs +++ b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs @@ -14,4 +14,7 @@ public partial class AsyncGeneratorMoveNextEmitter // The block-scope-rename + function-DC variable overrides that consume this live in the shared // IteratorMoveNextEmitter base (#1124). protected override IReadOnlyDictionary BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames; + + protected override string ResolveCaptureSourceName(Expr.ArrowFunction af, string capturedVar) => + PivotCaptureSource(_analysis.BlockScopeCaptureRenames, af, capturedVar); } diff --git a/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs b/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs index 2e74f2413..705679fa3 100644 --- a/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs @@ -63,85 +63,9 @@ private void EmitAsyncArrowFunction(Expr.ArrowFunction af) private void EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass) { - if (_ctx!.DisplayClassConstructors == null || !_ctx.DisplayClassConstructors.TryGetValue(af, out var displayCtor)) - { - _il.Emit(OpCodes.Ldnull); - SetStackUnknown(); - return; - } - - _il.Emit(OpCodes.Newobj, displayCtor); - - if (_ctx.ArrowEntryPointDCFields?.TryGetValue(af, out var entryPointDCField) == true && - _ctx.EntryPointDisplayClassStaticField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldsfld, _ctx.EntryPointDisplayClassStaticField); - _il.Emit(OpCodes.Stfld, entryPointDCField); - } - - // Populate $functionDC field if this arrow captures function-level variables - if (_ctx.ArrowFunctionDCFields?.TryGetValue(af, out var functionDCField) == true && - _builder.FunctionDCField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldarg_0); // state machine - _il.Emit(OpCodes.Ldfld, _builder.FunctionDCField); - _il.Emit(OpCodes.Stfld, functionDCField); - } - - if (_ctx.DisplayClassFields == null || !_ctx.DisplayClassFields.TryGetValue(af, out var fieldMap)) - { - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); - return; - } - - foreach (var (capturedVar, field) in fieldMap) - { - _il.Emit(OpCodes.Dup); - - // Per-iteration cell capture (#650): snapshot the StrongBox REFERENCE. - if (_ctx.CellBindingLocals.TryGetValue(capturedVar, out var cellLocal)) - { - _il.Emit(OpCodes.Ldloc, cellLocal); - _il.Emit(OpCodes.Stfld, field); - continue; - } - - // #767: pivot a captured nested-block shadow to its renamed storage (identity otherwise). - var sourceVar = PivotCaptureSource(_analysis.BlockScopeCaptureRenames, af, capturedVar); - - var hoistedField = _builder.GetVariableField(sourceVar); - if (hoistedField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, hoistedField); - } - else if (capturedVar == "this" && _builder.ThisField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, _builder.ThisField); - } - else if (_ctx.Locals.TryGetLocal(sourceVar, out var local)) - { - _il.Emit(OpCodes.Ldloc, local); - } - else if (!TryEmitGlobalVariable(sourceVar)) - { - // Preserve the historical unresolved-name fallback. Resolvable - // globals (notably function declarations with expando state) - // are loaded through the same canonical path as a normal read. - _il.Emit(OpCodes.Ldnull); - } - - _il.Emit(OpCodes.Stfld, field); - } - - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); + if (_ctx!.DisplayClassConstructors.TryGetValue(af, out var constructor)) + EmitCapturingArrowViaHooks(af, method, constructor); + else EmitNullConstant(); } private void EmitNonCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method) diff --git a/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs b/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs index 75f4d6200..a6facc75e 100644 --- a/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs +++ b/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs @@ -117,6 +117,10 @@ protected override void EmitTaggedTemplateLiteral(Expr.TaggedTemplateLiteral ttl protected override void EmitGetPrivate(Expr.GetPrivate gp) { + if (TryEmitPrivateMethodValue(gp)) return; + if (TryEmitDefinitionPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestStaticPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestInstancePrivateField(gp.Object, gp.Name)) return; if (TryEmitGenericPrivateGet(gp)) return; // Get the field name without the # prefix @@ -186,6 +190,9 @@ protected override void EmitGetPrivate(Expr.GetPrivate gp) protected override void EmitSetPrivate(Expr.SetPrivate sp) { + if (TryEmitDefinitionPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestStaticPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestInstancePrivateField(sp.Object, sp.Name, sp.Value)) return; if (TryEmitGenericPrivateSet(sp, SpillBoxed)) return; // Get the field name without the # prefix @@ -268,6 +275,7 @@ protected override void EmitSetPrivate(Expr.SetPrivate sp) protected override void EmitCallPrivate(Expr.CallPrivate cp) { + if (TryEmitDefinitionPrivateCall(cp)) return; if (TryEmitGenericPrivateCall(cp, SpillBoxed)) return; // Get the method name without the # prefix diff --git a/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs b/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs index 737275098..15b63d1d5 100644 --- a/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs +++ b/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs @@ -24,6 +24,7 @@ public partial class AsyncMoveNextEmitter : AsyncFunctionMoveNextEmitter, IEmitt protected override TypeProvider Types => _types; protected override IVariableResolver Resolver => _resolver!; protected override FieldBuilder? GetThisField() => _builder.ThisField; + protected override FieldBuilder? GetFunctionDCField() => _builder.FunctionDCField; #region IEmitterContext Implementation diff --git a/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs b/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs index cd53b9813..87afe44a4 100644 --- a/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs @@ -22,12 +22,21 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) !ctx.VarToClassExpr.TryGetValue(classExprVar.Name.Lexeme, out var classExpr) || ctx.ClassExprStaticMethods == null || !ctx.ClassExprStaticMethods.TryGetValue(classExpr, out var exprStaticMethods) || - !exprStaticMethods.TryGetValue(classExprGet.Name.Lexeme, out var exprStaticMethod)) + !exprStaticMethods.ContainsKey(classExprGet.Name.Lexeme) + || emitter.ArgsContainSuspension(call.Arguments)) return false; var il = emitter.IL; - emitter.EmitStaticCallArguments(call.Arguments, exprStaticMethod); - il.Emit(OpCodes.Call, exprStaticMethod); + emitter.EmitExpression(classExprGet.Object); + emitter.EnsureBoxed(); + var receiver = il.DeclareLocal(ctx.Types.Object); + il.Emit(OpCodes.Stloc, receiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldstr, classExprGet.Name.Lexeme); + il.Emit(OpCodes.Call, ctx.Runtime!.ObjectRead.Property); + emitter.EmitArgsArrayWithSpread(call.Arguments); + il.Emit(OpCodes.Call, ctx.Runtime.Invocation.Method); emitter.SetStackUnknown(); return true; } diff --git a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs index ed0aa41d8..85ae18d0b 100644 --- a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs @@ -22,6 +22,17 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) return false; var ctx = emitter.Context; + if (ctx.HasGuestReceiver && ctx.ClassDefinitionParameterIndex is { } definitionParameter) + { + emitter.IL.Emit(OpCodes.Ldarg, definitionParameter); + emitter.IL.Emit(OpCodes.Call, ctx.Runtime!.ClassDefinitions.GetParent); + emitter.IL.Emit(OpCodes.Ldarg_0); + emitter.EmitArgsArrayWithSpread(call.Arguments); + emitter.IL.Emit(OpCodes.Call, ctx.Runtime.ClassDefinitions.InitializeReceiver); + emitter.IL.Emit(OpCodes.Ldarg_0); + emitter.SetStackUnknown(); + return true; + } // The emitted base is chosen from checked declaration identity. Prefer its // expression-owned constructor before any same-named lexical declaration. @@ -41,7 +52,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) // Try class declaration constructors first var parentCtor = ctx.CurrentSuperclassName != null - ? ctx.ClassRegistry?.GetConstructor(ctx.CurrentSuperclassName) + ? ctx.ClassRegistry?.GetConstructorByQualifiedName(ctx.ResolveClassName(ctx.CurrentSuperclassName)) : null; if (parentCtor != null) { @@ -279,6 +290,17 @@ private static void EmitSuperCtorCall(IEmitterContext emitter, ConstructorBuilde // evaluating and discarding surplus values safely, it publishes the // exact caller list when the base constructor binds `arguments`. emitter.EmitStaticCallArguments(arguments, parentCtor); + if (ctx.CurrentClassExpr is { } expression + && ctx.ClassExprDefinitionFields?.TryGetValue(expression, out var definitionField) == true + && parentCtor.GetParameters().LastOrDefault()?.ParameterType == ctx.Runtime!.ClassDefinitions.Type) + { + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(definitionField)); + il.Emit(OpCodes.Castclass, ctx.Runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, ctx.Runtime.ClassDefinitions.Parent); + il.Emit(OpCodes.Isinst, ctx.Runtime.ClassDefinitions.Type); + } System.Reflection.ConstructorInfo ctorToCall = parentCtor; Type? baseType = ctx.CurrentClassBuilder?.BaseType; diff --git a/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs b/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs index 1a51ecc12..228056bda 100644 --- a/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs @@ -21,7 +21,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) var ctx = emitter.Context; // Only applies in static context - if (ctx.IsInstanceMethod || ctx.CurrentClassBuilder == null) + if (ctx.IsInstanceMethod || ctx.ClassDefinitionParameterIndex != null || ctx.ClassDefinitionVariableName != null || ctx.CurrentClassBuilder == null) return false; string? currentClassName = ctx.CurrentClassName; diff --git a/src/SharpTS/Compilation/ClassExpressionFactory.cs b/src/SharpTS/Compilation/ClassExpressionFactory.cs new file mode 100644 index 000000000..202b1a66e --- /dev/null +++ b/src/SharpTS/Compilation/ClassExpressionFactory.cs @@ -0,0 +1,7 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +internal sealed record ClassExpressionFactory(MethodBuilder Method, Type Template, string Name, int Length, + MethodBuilder Initializer, IReadOnlyList Keys); diff --git a/src/SharpTS/Compilation/ClosureAnalyzer.cs b/src/SharpTS/Compilation/ClosureAnalyzer.cs index 29d5837e6..833130703 100644 --- a/src/SharpTS/Compilation/ClosureAnalyzer.cs +++ b/src/SharpTS/Compilation/ClosureAnalyzer.cs @@ -41,6 +41,7 @@ public class ClosureAnalyzer : AstVisitorBase // Stack of function scopes - tracks the function node at each nesting level private readonly Stack _functionStack = new(); + private readonly Stack<(Expr.ClassExpr Expression, HashSet EnclosingFunctions)> _classCaptureOwners = new(); // Maps function node → set of its local variables that are captured by inner closures private readonly Dictionary> _functionCapturedLocals = []; @@ -399,6 +400,9 @@ private void ReferenceVariable(string name) // This enables function-level display class creation // Walk up the function stack to find which function defines this variable var definingFunc = FindDefiningFunction(name); + foreach (var (expression, enclosingFunctions) in _classCaptureOwners) + if (definingFunc == null || enclosingFunctions.Contains(definingFunc)) + _captures[expression].Add(name); if (definingFunc != null) { // The variable is defined in some function, not at top level @@ -679,12 +683,12 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) // Class expressions don't declare the class name in the outer scope // (unlike class declarations), but we still need to analyze all bodies + _classCaptureOwners.Push((expr, _functionStack.ToHashSet())); // Analyze field initializers for captured variables - foreach (var field in expr.Fields) - { - if (field.Initializer != null) - Visit(field.Initializer); - } + AnalyzeFunctionBody(expr, [], expr.Fields.Where(field => field.Initializer != null) + .Select(field => (Stmt)new Stmt.Expression(field.Initializer!)) + .Concat((expr.StaticInitializers ?? []).OfType().SelectMany(block => block.Body)) + .ToList()); // Analyze methods foreach (var method in expr.Methods) @@ -705,6 +709,7 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) AnalyzeFunctionBody(accessor, parameters, accessor.Body); } } + _classCaptureOwners.Pop(); } protected override void VisitBlock(Stmt.Block stmt) diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index 424da11a4..f42a4514f 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -11,6 +11,9 @@ public partial class CompilationContext // Class expression builders (class expr node -> type builder) public Dictionary? ClassExprBuilders { get; set; } + internal Dictionary? ClassExprFactories { get; set; } + internal Dictionary? ClassExprDefinitionFields { get; set; } + internal Dictionary? RuntimeClassDeclarations { get; set; } // Class expression extended tracking public Dictionary>? ClassExprBackingFields { get; set; } @@ -26,10 +29,18 @@ public partial class CompilationContext public Dictionary? ClassExprConstructors { get; set; } public ClassGenericParameterRegistry? ClassGenericParameters { get; set; } public Dictionary? ClassExprSuperclass { get; set; } - public Dictionary>? ClassExprCaptureFields { get; set; } + public Dictionary>? ClassExprCaptureSlots { get; set; } + internal const string PrivateOwnerCaptureName = "'privateOwner"; + internal IReadOnlyDictionary? ClassExprEnclosingClasses { get; set; } // Current class expression being compiled public Expr.ClassExpr? CurrentClassExpr { get; set; } + internal int? ClassDefinitionParameterIndex { get; set; } + internal int? ClassDefinitionThisParameterIndex { get; set; } + internal string? ClassDefinitionVariableName { get; set; } + internal FieldBuilder? ClassDefinitionOwnerField { get; set; } + internal string? GuestThisVariableName { get; set; } + internal bool HasGuestReceiver { get; set; } // Variable name to class expression mapping (for static member access) public Dictionary? VarToClassExpr { get; set; } diff --git a/src/SharpTS/Compilation/CompilationContext.Classes.cs b/src/SharpTS/Compilation/CompilationContext.Classes.cs index 980205a31..1a130f237 100644 --- a/src/SharpTS/Compilation/CompilationContext.Classes.cs +++ b/src/SharpTS/Compilation/CompilationContext.Classes.cs @@ -160,6 +160,15 @@ public string ResolvePrivateMethodOwner(string fallbackClassName, string methodN /// public string ResolveClassName(string simpleClassName) { + var namespacePath = CurrentNamespacePath; + var declarationModule = IsScriptTopLevel ? null : CurrentModulePath; + while (namespacePath is not null) + { + if (ScopedClassNames.TryGetValue((declarationModule, namespacePath, simpleClassName), out var scoped)) return scoped; + int dot = namespacePath.LastIndexOf('.'); + namespacePath = dot < 0 ? null : namespacePath[..dot]; + } + if (ScopedClassNames.TryGetValue((declarationModule, null, simpleClassName), out var local)) return local; // A local declaration wins over the shared import/name index, which can // also contain a same-named class from another module. if (CurrentModulePath is not null) @@ -196,6 +205,33 @@ public string ResolveClassName(string simpleClassName) return baseName; } + internal IReadOnlyDictionary CheckedClassNames { get; set; } = new Dictionary(); + internal IReadOnlyDictionary<(string? Module, string? Namespace, string Name), string> ScopedClassNames { get; set; } + = new Dictionary<(string?, string?, string), string>(); + + internal string ResolveClassName(SharpTS.TypeSystem.TypeInfo? type, string fallback) + { + int? declaration = type switch + { + SharpTS.TypeSystem.TypeInfo.Class c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.GenericClass c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.MutableClass c => c.DeclarationId, + SharpTS.TypeSystem.TypeInfo.Instance i => GetCheckedClassDeclaration(i.ClassType), + SharpTS.TypeSystem.TypeInfo.InstantiatedGeneric i => GetCheckedClassDeclaration(i.GenericDefinition), + _ => null + }; + return declaration is { } id && CheckedClassNames.TryGetValue(id, out var name) ? name : ResolveClassName(fallback); + } + + private static int? GetCheckedClassDeclaration(SharpTS.TypeSystem.TypeInfo type) => type switch + { + SharpTS.TypeSystem.TypeInfo.Class c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.GenericClass c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.MutableClass c => c.DeclarationId, + SharpTS.TypeSystem.TypeInfo.InstantiatedGeneric i => GetCheckedClassDeclaration(i.GenericDefinition), + _ => null + }; + /// /// Gets the qualified class name for the current module context. /// Also applies .NET namespace if set via @Namespace directive. diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs new file mode 100644 index 000000000..b7fcdad19 --- /dev/null +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -0,0 +1,88 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +/// Guest class evaluations, independent of their pre-emitted CLR templates. +public sealed class EmittedClassDefinitionRuntime +{ + internal EmittedClassDefinitionRuntime() { } + internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, + MethodInfo GetDefinition, ConstructorBuilder Constructor, FieldBuilder Template, + FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, FieldBuilder PrivateMembers, MethodBuilder? Create, MethodBuilder Construct, + MethodBuilder ReadProperty, MethodBuilder Invoke, TypeBuilder? CaptureType, ConstructorBuilder? CaptureConstructor, + FieldBuilder? CaptureOwner, FieldBuilder? CaptureField, MethodBuilder? ReadCapture, MethodBuilder? WriteCapture, + MethodBuilder? FindEnvironment, MethodBuilder? ValidateParent, MethodBuilder? InitializeReceiver, + MethodBuilder? GetParent, MethodBuilder? ReadArgument, MethodBuilder? ReadPrototypeProperty, MethodBuilder? ReadSuper); + + private Declarations? _declarations; + private Declarations Required => _declarations + ?? throw new InvalidOperationException("Class-definition metadata has not been declared."); + public bool IsComplete { get; private set; } + public TypeBuilder Type => Required.Type; + public Type InstanceInterface => Required.InstanceInterface; + public MethodInfo GetDefinition => Required.GetDefinition; + public ConstructorBuilder Constructor => Required.Constructor; + public FieldBuilder Template => Required.Template; + public FieldBuilder Factory => Required.Factory; + public FieldBuilder Prototype => Required.Prototype; + public FieldBuilder Keys => Required.Keys; + public FieldBuilder Parent => Required.Parent; + public FieldBuilder Captures => Required.Captures; + public FieldBuilder PrivateMembers => Required.PrivateMembers; + public MethodBuilder Create => RequireRuntimeParentHelper(Required.Create); + public MethodBuilder Construct => Required.Construct; + public MethodBuilder ReadProperty => Required.ReadProperty; + public MethodBuilder Invoke => Required.Invoke; + public ConstructorBuilder CaptureConstructor => Required.CaptureConstructor + ?? throw new InvalidOperationException("User-class runtime helpers were not selected."); + public MethodBuilder ReadCapture => RequireRuntimeParentHelper(Required.ReadCapture); + public MethodBuilder WriteCapture => RequireRuntimeParentHelper(Required.WriteCapture); + public MethodBuilder FindEnvironment => RequireRuntimeParentHelper(Required.FindEnvironment); + internal bool HasRuntimeParentHelpers => Required.ValidateParent is not null; + public MethodBuilder ValidateParent => RequireRuntimeParentHelper(Required.ValidateParent); + public MethodBuilder InitializeReceiver => RequireRuntimeParentHelper(Required.InitializeReceiver); + public MethodBuilder GetParent => RequireRuntimeParentHelper(Required.GetParent); + public MethodBuilder ReadArgument => RequireRuntimeParentHelper(Required.ReadArgument); + public MethodBuilder ReadPrototypeProperty => RequireRuntimeParentHelper(Required.ReadPrototypeProperty); + public MethodBuilder ReadSuper => RequireRuntimeParentHelper(Required.ReadSuper); + + private static MethodBuilder RequireRuntimeParentHelper(MethodBuilder? method) => method + ?? throw new InvalidOperationException("User-class runtime helpers were not selected."); + + internal void Declare(Declarations declarations) + { + ArgumentNullException.ThrowIfNull(declarations); + if (_declarations is not null || IsComplete) + throw new InvalidOperationException("Class-definition metadata has already been declared."); + if (new MemberInfo?[] { declarations.Constructor, declarations.Template, declarations.Factory, + declarations.Prototype, declarations.Keys, declarations.Parent, declarations.Captures, declarations.PrivateMembers, declarations.Create, declarations.Construct, + declarations.ReadProperty, declarations.Invoke, declarations.ReadCapture, declarations.WriteCapture, + declarations.FindEnvironment, declarations.ValidateParent, declarations.InitializeReceiver, + declarations.GetParent, declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } + .Any(member => member is not null && member.DeclaringType != declarations.Type) + || declarations.GetDefinition.DeclaringType != declarations.InstanceInterface + // A completed interface exposes RuntimeModule, while its in-memory + // owner still exposes RuntimeModuleBuilder. Their MVID is the same + // module identity even though the reflection wrappers differ. + || declarations.InstanceInterface.Module.ModuleVersionId != declarations.Type.Module.ModuleVersionId + || (declarations.CaptureType is not null && declarations.CaptureType.Module != declarations.Type.Module) + || new MemberInfo?[] { declarations.CaptureConstructor, declarations.CaptureOwner, declarations.CaptureField } + .Any(member => member is not null && member.DeclaringType != declarations.CaptureType)) + throw new InvalidOperationException("Class-definition handles must belong to their declared owner."); + _declarations = declarations; + } + + internal void CompleteEmission() + { + var declarations = Required; + if (IsComplete) throw new InvalidOperationException("Class-definition emission is already complete."); + if (new[] { declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke, + declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment, + declarations.ValidateParent, declarations.InitializeReceiver, declarations.GetParent, + declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } + .Any(method => method is not null && method.GetILGenerator().ILOffset == 0)) + throw new InvalidOperationException("Class-definition method bodies have not all been emitted."); + IsComplete = true; + } +} diff --git a/src/SharpTS/Compilation/EmittedRuntime.cs b/src/SharpTS/Compilation/EmittedRuntime.cs index 3d71a1099..fa43b3bcd 100644 --- a/src/SharpTS/Compilation/EmittedRuntime.cs +++ b/src/SharpTS/Compilation/EmittedRuntime.cs @@ -146,6 +146,7 @@ public EmittedStableIteratorResultRuntime RequireStableIteratorResults() => Stab /// Required user-class prototype declarations and registration for one compilation. public EmittedClassPrototypeRuntime ClassPrototypes { get; } = new(); + public EmittedClassDefinitionRuntime ClassDefinitions { get; } = new(); /// Required public Object descriptor operations for this compilation. diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index 48d80e0ca..d951c1281 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -30,6 +30,8 @@ public abstract partial class ExpressionEmitterBase /// protected virtual void EmitCall(Expr.Call c) { + if (TryEmitGuestSuperCall(c)) return; + if (TryEmitGuestThisCall(c)) return; if (TryEmitNumericRestCompanionCall(c)) return; if (TryEmitStaticIndirectEvalGlobal(c)) return; @@ -1312,6 +1314,7 @@ protected static bool AnyContainsSuspension(IEnumerable exprs) Expr.CallPrivate cp => ExprContainsSuspension(cp.Object) || AnyContainsSuspension(cp.Arguments), Expr.Get get => ExprContainsSuspension(get.Object), Expr.GetPrivate gp => ExprContainsSuspension(gp.Object), + Expr.PrivateIn presence => ExprContainsSuspension(presence.Object), Expr.Set s => ExprContainsSuspension(s.Object) || ExprContainsSuspension(s.Value), Expr.SetPrivate sp => ExprContainsSuspension(sp.Object) || ExprContainsSuspension(sp.Value), Expr.GetIndex gi => ExprContainsSuspension(gi.Object) || ExprContainsSuspension(gi.Index), @@ -2174,9 +2177,10 @@ protected string ResolveInstanceClassName(Expr receiver, string simpleClassName) { var (parts, name) = ExtractQualifiedNameFromCallee(construction.Callee); if (name.Length > 0) - return ResolveClassNameForNew(parts, name); + return Ctx.ResolveClassName(Ctx.TypeMap?.Get(construction.Callee), + ResolveClassNameForNew(parts, name)); } - return Ctx.ResolveClassName(simpleClassName); + return Ctx.ResolveClassName(Ctx.TypeMap?.Get(receiver), simpleClassName); } protected bool TryEmitDirectMethodCall(Expr receiver, string methodName, List arguments) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs new file mode 100644 index 000000000..cf58ed930 --- /dev/null +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs @@ -0,0 +1,342 @@ +using System.Reflection.Emit; +using System.Reflection; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public abstract partial class ExpressionEmitterBase +{ + protected bool TryEmitGuestSuperCall(Expr.Call call) + { + if (!Ctx.HasGuestReceiver || call.Callee is not Expr.Super { Method: not null } super + || super.Method.Lexeme == "constructor") return false; + var receiver = SpillBoxed(new Expr.This(new Token(TokenType.THIS, "this", null, 0))); + EmitGuestSuperValue(super.Method.Lexeme, receiver); + EmitGuestMethodInvocation(receiver, call.Arguments); + return true; + } + + protected void EmitGuestSuperValue(string name, LocalBuilder receiver) + { + if (Ctx.CurrentClassExpr != null && TryEmitOwnedClassDefinition()) { } + else + { + IL.Emit(OpCodes.Ldtoken, Ctx.CurrentClassBuilder!); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + } + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Ldstr, name); + IL.Emit(Ctx.IsInstanceMethod ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.ReadSuper); + SetStackUnknown(); + } + + protected bool TryEmitGuestThisSet(Expr.Set set) + { + if (set.Object is not Expr.This || (!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null)) return false; + var receiver = SpillBoxed(set.Object); + var value = SpillBoxed(set.Value); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldstr, set.Name.Lexeme); IL.Emit(OpCodes.Ldloc, value); + IL.Emit(OpCodes.Ldc_I4_1); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.PropertyStrict); + IL.Emit(OpCodes.Ldloc, value); + SetStackUnknown(); + return true; + } + + protected bool TryEmitGuestThisCall(Expr.Call call) + { + if (call.Callee is not Expr.Get { Object: Expr.This, Optional: false } get || call.Optional + || (!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null)) return false; + var receiver = SpillBoxed(get.Object); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectRead.Property); + EmitGuestMethodInvocation(receiver, call.Arguments); + return true; + } + + protected void EmitGuestMethodInvocation(LocalBuilder receiver, IReadOnlyList argumentExpressions) + { + var callable = _helpers.SpillStoreObject(); + EmitArgsArrayWithSpread(argumentExpressions); + var arguments = _helpers.SpillStoreObject(); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloc, callable); IL.Emit(OpCodes.Ldloc, arguments); + IL.Emit(OpCodes.Castclass, Types.ObjectArray); IL.Emit(OpCodes.Call, Ctx.Runtime!.Invocation.Method); + SetStackUnknown(); + } + + protected bool TryEmitClassDefinitionSelf(string name) + { + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? + .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + if (expression?.Name?.Lexeme != name) return false; + if (!TryEmitOwnedClassDefinition()) return false; + SetStackUnknown(); + return true; + } + + protected bool TryEmitOwnedClassDefinition() + { + if (Ctx.ClassDefinitionOwnerField is { } ownerField) + { + IL.Emit(OpCodes.Ldarg_0); + if (GetThisField() is { } stateReceiver) + { + IL.Emit(OpCodes.Ldfld, stateReceiver); + IL.Emit(OpCodes.Castclass, ownerField.DeclaringType!); + } + IL.Emit(OpCodes.Ldfld, ownerField); + } + else if (Ctx.ClassDefinitionVariableName is { } variable) + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, variable, null, 0))); + else if (Ctx.ClassDefinitionParameterIndex is { } parameter) + IL.Emit(OpCodes.Ldarg, parameter); + else + { + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? + .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + if (!Ctx.IsInstanceMethod || expression == null || + Ctx.ClassExprDefinitionFields?.TryGetValue(expression, out var field) != true) return false; + EmitThis(); + // Read the lexical owner's field: a derived instance can carry a + // distinct child definition while executing this base method. + var reference = EmitterTypeHelpers.SelfFieldReference(field!); + if (field!.DeclaringType!.IsGenericTypeDefinition && Ctx.ClassExprFactories?.TryGetValue(expression, out var factory) == true) + reference = EmitterTypeHelpers.ResolveField(factory.Template, field); + IL.Emit(OpCodes.Castclass, reference.DeclaringType!); + IL.Emit(OpCodes.Ldfld, reference); + } + IL.Emit(OpCodes.Castclass, Ctx.Runtime!.ClassDefinitions.Type); + SetStackUnknown(); + return true; + } + + protected bool TryEmitGuestThis() + { + if (Ctx.GuestThisVariableName is not { } name) return false; + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, name, null, 0))); + EnsureBoxed(); + return true; + } + + protected bool TryEmitGuestThisGet(Expr.Get get) + { + if (get.Object is not Expr.This || get.Optional) return false; + if (!TryEmitGuestThis()) + { + if (!Ctx.HasGuestReceiver) return false; + EmitThis(); + EnsureBoxed(); + } + IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectRead.Property); + SetStackUnknown(); + return true; + } + + protected bool TryEmitClassDefinitionCapture(string name) + { + if (!TryGetClassDefinitionCaptureSlot(name, out var slot) || !TryEmitOwnedClassDefinition()) return false; + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.Captures); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldelem_Ref); + IL.Emit(OpCodes.Call, Ctx.Runtime.ClassDefinitions.ReadCapture); + Ctx.EmitLexicalTdzValueCheck(IL, name); + SetStackUnknown(); + return true; + } + + protected bool TryGetClassDefinitionCaptureSlot(string name, out int slot) + { + slot = -1; + if (Ctx.TryGetParameter(name, out _) || Ctx.Locals.GetLocal(name) != null || GetHoistedVariableField(name) != null) return false; + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? + .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + if (expression == null || Ctx.ClassExprCaptureSlots?.TryGetValue(expression, out var captures) != true + || !captures!.TryGetValue(name, out slot)) return false; + return true; + } + + protected bool TryEmitStoreClassDefinitionCapture(string name) + { + if (!TryGetClassDefinitionCaptureSlot(name, out var slot)) return false; + var value = IL.DeclareLocal(Types.Object); + IL.Emit(OpCodes.Stloc, value); + if (!TryEmitOwnedClassDefinition()) throw new InvalidOperationException("Capture has no class definition owner."); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldloc, value); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.WriteCapture); + return true; + } + + protected bool TryEmitClassDefinitionEnvironment(Type environment) + { + if (!TryEmitOwnedClassDefinition()) return false; + IL.Emit(OpCodes.Ldtoken, environment); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.FindEnvironment); + IL.Emit(OpCodes.Castclass, environment); + return true; + } + + protected void EmitGuestClassDefinition(Expr.ClassExpr expression) + { + if (Ctx.ClassExprFactories?.TryGetValue(expression, out var factory) != true) + throw new InvalidOperationException("Class expression factory has not been declared."); + if (expression.SuperclassExpr is { } heritage) + { + EmitClassHeritageValue(heritage, expression.Name?.Lexeme); + EnsureBoxed(); + if ((factory!.Template.BaseType == null || factory.Template.BaseType == Types.Object) + && heritage is not Expr.ClassExpr { Methods.Count: 0, Fields.Count: 0 }) + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.ValidateParent); + } + else IL.Emit(OpCodes.Ldnull); + var parent = _helpers.SpillStoreObject(); + IL.Emit(OpCodes.Ldtoken, factory!.Template); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassInitialization.RunDefinition); + var keys = IL.DeclareLocal(Types.ObjectArray); + EmitDefinitionKeyArray(factory.Keys); + IL.Emit(OpCodes.Stloc, keys); + var captures = IL.DeclareLocal(Types.ObjectArray); + EmitDefinitionCaptureArray(Ctx.ClassExprCaptureSlots!.GetValueOrDefault(expression)?.Keys.ToArray() ?? []); + IL.Emit(OpCodes.Stloc, captures); + IL.Emit(OpCodes.Ldtoken, factory.Template); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Ldnull); + IL.Emit(OpCodes.Ldftn, factory.Method); + IL.Emit(OpCodes.Newobj, Types.GetConstructor(typeof(Func), Types.Object, Types.IntPtr)); + IL.Emit(OpCodes.Ldstr, factory.Name); + IL.Emit(OpCodes.Ldc_R8, (double)factory.Length); + IL.Emit(OpCodes.Ldloc, parent); + IL.Emit(OpCodes.Ldloc, keys); + IL.Emit(OpCodes.Ldloc, captures); + IL.Emit(OpCodes.Call, Ctx.Runtime.ClassDefinitions.Create); + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Call, factory.Initializer); + SetStackUnknown(); + } + + protected void EmitDefinitionComputedKeys(MethodBuilder method, IReadOnlyList keys) + { + EmitDefinitionKeyArray(keys); + IL.Emit(OpCodes.Call, method); + } + + private void EmitDefinitionCaptureArray(IReadOnlyList names) + { + var values = new List(names.Count); + foreach (var name in names) + { + if (name == CompilationContext.PrivateOwnerCaptureName) + { + if (!TryEmitOwnedClassDefinition()) + throw new InvalidOperationException("Nested private names require their enclosing class evaluation."); + values.Add(_helpers.SpillStoreObject()); + continue; + } + FieldInfo? field = null; + if (Ctx.CellBindingLocals.TryGetValue(name, out var cell)) + { + IL.Emit(OpCodes.Ldloc, cell); + field = Types.StrongBoxOfObjectValueField; + } + else if (Ctx.FunctionDisplayClassFields?.TryGetValue(name, out var functionField) == true + && Ctx.FunctionDisplayClassLocal != null) + { + IL.Emit(OpCodes.Ldloc, Ctx.FunctionDisplayClassLocal); + field = functionField; + } + else if (Ctx.FunctionDisplayClassFields?.TryGetValue(name, out functionField) == true + && GetFunctionDCField() is { } stateField) + { + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, stateField); + field = functionField; + } + else if (Ctx.FunctionDisplayClassFields?.TryGetValue(name, out functionField) == true + && Ctx.CurrentArrowFunctionDCField != null) + { + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, Ctx.CurrentArrowFunctionDCField); + field = functionField; + } + else if (Ctx.CapturedFields?.TryGetValue(name, out var capturedField) == true) + { + IL.Emit(OpCodes.Ldarg_0); + field = capturedField; + } + else if (Ctx.EntryPointDisplayClassFields?.TryGetValue(name, out var entryField) == true + && (Ctx.EntryPointDisplayClassLocal != null || Ctx.EntryPointDisplayClassStaticField != null)) + { + if (Ctx.EntryPointDisplayClassLocal != null) IL.Emit(OpCodes.Ldloc, Ctx.EntryPointDisplayClassLocal); + else IL.Emit(OpCodes.Ldsfld, Ctx.EntryPointDisplayClassStaticField!); + field = entryField; + } + else if (Ctx.TopLevelStaticVars?.TryGetValue(name, out var staticField) == true) + { + IL.Emit(OpCodes.Ldnull); + field = staticField; + } + if (field != null) + { + IL.Emit(OpCodes.Ldtoken, field); + IL.Emit(OpCodes.Ldtoken, field.DeclaringType!); + IL.Emit(OpCodes.Call, Types.GetMethod(Types.FieldInfo, "GetFieldFromHandle", + Types.Resolve("System.RuntimeFieldHandle"), Types.Resolve("System.RuntimeTypeHandle"))); + IL.Emit(OpCodes.Newobj, Ctx.Runtime!.ClassDefinitions.CaptureConstructor); + } + else if (TryGetClassDefinitionCaptureSlot(name, out var slot) && TryEmitOwnedClassDefinition()) + { + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.Captures); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldelem_Ref); + } + else + { + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, name, null, 0))); + EnsureBoxed(); + } + values.Add(_helpers.SpillStoreObject()); + } + IL.Emit(OpCodes.Ldc_I4, values.Count); + IL.Emit(OpCodes.Newarr, Types.Object); + for (int i = 0; i < values.Count; i++) + { + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Ldc_I4, i); + IL.Emit(OpCodes.Ldloc, values[i]); + IL.Emit(OpCodes.Stelem_Ref); + } + } + + protected void EmitDefinitionKeyArray(IReadOnlyList keys, bool coerceKeys = true) + { + var values = new List(keys.Count); + foreach (var key in keys) + { + EmitExpression(key); + EnsureBoxed(); + if (coerceKeys) + { + var isSymbol = IL.DefineLabel(); + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); + IL.Emit(OpCodes.Brtrue, isSymbol); + IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); + IL.MarkLabel(isSymbol); + } + values.Add(_helpers.SpillStoreObject()); + } + IL.Emit(OpCodes.Ldc_I4, values.Count); + IL.Emit(OpCodes.Newarr, Types.Object); + for (int i = 0; i < values.Count; i++) + { + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Ldc_I4, i); + IL.Emit(OpCodes.Ldloc, values[i]); + IL.Emit(OpCodes.Stelem_Ref); + } + } +} diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs index 8f15da458..11add3223 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.Operators.cs @@ -78,6 +78,7 @@ protected void EmitCompoundOperation(TokenType opType) /// protected virtual void EmitStoreVariable(string name) { + if (TryEmitStoreClassDefinitionCapture(name)) return; // Per-iteration loop-binding cell (#650): write through the StrongBox. This base // helper (used by state-machine increments/compound/logical assignment) hand-rolls // the store instead of routing through the resolver, so the cell case is handled here. diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs index 8640ad11c..6f90657ba 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs @@ -59,9 +59,7 @@ private LocalBuilder SpillGenericPrivateOperand(Expr expression) { EmitExpression(expression); EnsureBoxed(); - var local = IL.DeclareLocal(typeof(object)); - IL.Emit(OpCodes.Stloc, local); - return local; + return _helpers.SpillStoreObject(); } protected bool TryEmitGenericPrivateCall(Expr.CallPrivate call, Func? spill = null) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs new file mode 100644 index 000000000..495e871b6 --- /dev/null +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs @@ -0,0 +1,151 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public abstract partial class ExpressionEmitterBase +{ + protected virtual void EmitPrivateIn(Expr.PrivateIn expression) + { + string name = expression.Name.Lexeme.TrimStart('#'); + string current = Ctx.CurrentClassName ?? throw new InvalidOperationException("Private name has no lexical owner."); + string owner = Ctx.TypeMap?.GetPrivateInOwner(expression.Name) is { } checkedOwner + ? Ctx.ResolveClassName(checkedOwner, checkedOwner.Name) + : new[] { current }.Concat(Ctx.EnclosingClassNames ?? []) + .First(className => Ctx.ClassRegistry?.GetPrivateElements(className) is { } elements + && (elements.FieldNames.Contains(name) || elements.StaticFields.ContainsKey(name) + || elements.Methods.ContainsKey(name) || elements.StaticMethods.ContainsKey(name))); + var declaration = Ctx.ClassRegistry!.GetPrivateElements(owner)!; + var receiver = SpillGenericPrivateOperand(expression.Object); + var invalid = IL.DefineLabel(); + var absent = IL.DefineLabel(); + var done = IL.DefineLabel(); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Brfalse, invalid); + foreach (var primitive in new Type[] { Types.String, Types.Double, Types.Boolean, Types.BigInteger, + Ctx.Runtime!.Sentinels.UndefinedType, Ctx.Runtime.Symbols.Type }) + { + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Isinst, primitive); + IL.Emit(OpCodes.Brtrue, invalid); + } + + bool isStatic = declaration.StaticFields.ContainsKey(name) || declaration.StaticMethods.ContainsKey(name); + if (TryEmitPrivateInOwnerDefinition(owner)) + { + var definition = IL.DeclareLocal(Ctx.Runtime!.ClassDefinitions.Type); + IL.Emit(OpCodes.Stloc, definition); + if (isStatic) IL.Emit(OpCodes.Ldloc, receiver); + else + { + var ownerExpression = Ctx.ClassExprBuilders!.First(entry => entry.Value.FullName == owner).Key; + var template = Ctx.ClassExprFactories![ownerExpression].Template; + var field = Ctx.ClassExprDefinitionFields![ownerExpression]; + var reference = template.IsGenericType ? EmitterTypeHelpers.ResolveField(template, field) : field; + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Isinst, reference.DeclaringType!); + IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Castclass, reference.DeclaringType!); + IL.Emit(OpCodes.Ldfld, reference); + } + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ceq); + IL.Emit(OpCodes.Brfalse, absent); + if (isStatic) + { + if (declaration.StaticMethods.ContainsKey(name)) IL.Emit(OpCodes.Ldc_I4_1); + else + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ldfld, Ctx.Runtime.ClassDefinitions.PrivateMembers); + IL.Emit(OpCodes.Ldstr, $"field:{name}"); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "ContainsKey", Types.String)); + } + IL.Emit(OpCodes.Br, done); + } + else EmitInstancePresence(); + } + else if (isStatic) + { + Ctx.ClassRegistry.TryGetClass(owner, out var builder); + if (builder!.IsGenericTypeDefinition) + { + var type = IL.DeclareLocal(Types.Type); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Isinst, Types.Type); IL.Emit(OpCodes.Stloc, type); + IL.Emit(OpCodes.Ldloc, type); IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, type); IL.Emit(OpCodes.Callvirt, Types.GetProperty(Types.Type, "IsGenericType").GetGetMethod()!); + IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, type); IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.Type, "GetGenericTypeDefinition")); + } + else IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Ldtoken, builder); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Ceq); IL.Emit(OpCodes.Brfalse, absent); + if (declaration.StaticMethods.ContainsKey(name)) IL.Emit(OpCodes.Ldc_I4_1); + else + { + Ctx.ClassRegistry.TryGetCallableStaticPrivateField(owner, name, out var ownerField); + var presence = ownerField!.DeclaringType!.IsConstructedGenericType + ? EmitterTypeHelpers.ResolveField(ownerField.DeclaringType, declaration.StaticPresence!) + : declaration.StaticPresence!; + IL.Emit(OpCodes.Ldsfld, presence); + IL.Emit(OpCodes.Ldstr, name); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(typeof(HashSet), "Contains", Types.String)); + } + IL.Emit(OpCodes.Br, done); + } + else EmitInstancePresence(); + + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, "Right-hand side of private 'in' is not an object"); + IL.MarkLabel(absent); IL.Emit(OpCodes.Ldc_I4_0); + IL.MarkLabel(done); + SetStackType(StackType.Boolean); + + void EmitInstancePresence() + { + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + if (declaration.InstanceBridge is { } bridge) + { + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Isinst, bridge.InterfaceType); IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Castclass, bridge.InterfaceType); + IL.Emit(OpCodes.Callvirt, bridge.StorageGetter); + } + else IL.Emit(OpCodes.Ldsfld, declaration.Storage!); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloca, slots); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(declaration.Storage!.FieldType, "TryGetValue", Types.Object, Types.DictionaryStringObject.MakeByRefType())); + IL.Emit(OpCodes.Brfalse, absent); + if (declaration.Methods.ContainsKey(name)) IL.Emit(OpCodes.Ldc_I4_1); + else + { + IL.Emit(OpCodes.Ldloc, slots); IL.Emit(OpCodes.Ldstr, name); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "ContainsKey", Types.String)); + } + IL.Emit(OpCodes.Br, done); + } + + } + + private bool TryEmitPrivateInOwnerDefinition(string owner) + { + var ownerExpression = Ctx.ClassExprBuilders?.FirstOrDefault(entry => entry.Value.FullName == owner).Key; + if (ownerExpression is null) + return false; + var ownerBuilder = Ctx.ClassExprBuilders![ownerExpression]; + if (!TryEmitOwnedClassDefinition()) + throw new InvalidOperationException("Private name has no class evaluation owner."); + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders! + .First(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + while (!ReferenceEquals(Ctx.ClassExprBuilders![expression], ownerBuilder)) + { + int slot = Ctx.ClassExprCaptureSlots![expression][CompilationContext.PrivateOwnerCaptureName]; + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.Captures); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldelem_Ref); + IL.Emit(OpCodes.Castclass, Ctx.Runtime.ClassDefinitions.Type); + string parent = Ctx.ClassExprEnclosingClasses![expression]; + expression = Ctx.ClassExprBuilders.First(entry => entry.Value.FullName == parent).Key; + } + return true; + } +} diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs new file mode 100644 index 000000000..c233a70b1 --- /dev/null +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs @@ -0,0 +1,238 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public abstract partial class ExpressionEmitterBase +{ + protected bool TryEmitGuestInstancePrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) + { + if ((!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null) || Ctx.CurrentClassName is not { } current) return false; + string name = token.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateFieldOwner(current, name); + var declaration = Ctx.ClassRegistry?.GetPrivateElements(owner); + if (declaration?.FieldNames.Contains(name) != true) return false; + var receiver = SpillGenericPrivateOperand(receiverExpression); + var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); + var slots = declaration.InstanceBridge is { } bridge ? EmitGenericPrivateBrandCheck(bridge, receiver, name) + : EmitOrdinaryPrivateBrandCheck(owner, receiver, name); + IL.Emit(OpCodes.Ldloc, slots); IL.Emit(OpCodes.Ldstr, name); + if (value == null) IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "get_Item", Types.String)); + else + { + IL.Emit(OpCodes.Ldloc, value); IL.Emit(OpCodes.Callvirt, Types.DictionaryStringObjectSetItem); IL.Emit(OpCodes.Ldloc, value); + } + SetStackUnknown(); + return true; + } + + private LocalBuilder EmitOrdinaryPrivateBrandCheck(string owner, LocalBuilder receiver, string name) + { + var table = typeof(System.Runtime.CompilerServices.ConditionalWeakTable>); + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + var invalid = IL.DefineLabel(); + var valid = IL.DefineLabel(); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldsfld, Ctx.ClassRegistry!.GetPrivateFieldStorage(owner)!); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloca, slots); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(table, "TryGetValue", Types.Object, Types.DictionaryStringObject.MakeByRefType())); + IL.Emit(OpCodes.Brtrue, valid); + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} from an object whose class did not declare it"); + IL.MarkLabel(valid); + return slots; + } + + protected bool TryEmitGuestStaticPrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) + { + if ((Ctx.GuestThisVariableName == null && assignedValue == null) || Ctx.CurrentClassName is not { } current) return false; + string name = token.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateFieldOwner(current, name); + var declaration = Ctx.ClassRegistry?.GetPrivateElements(owner); + if (declaration?.StaticFields.ContainsKey(name) != true) return false; + var receiver = SpillGenericPrivateOperand(receiverExpression); + var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); + EmitOrdinaryStaticPrivateBrandCheck(owner, receiver, name); + if (!Ctx.ClassRegistry!.TryGetCallableStaticPrivateField(owner, name, out var field)) + throw new InvalidOperationException("Static private field has no callable owner."); + if (value == null) IL.Emit(OpCodes.Ldsfld, field!); + else + { + // A successful receiver brand check does not install a later field. + var presence = field!.DeclaringType!.IsConstructedGenericType + ? EmitterTypeHelpers.ResolveField(field.DeclaringType, declaration.StaticPresence!) + : declaration.StaticPresence!; + var installed = IL.DefineLabel(); + IL.Emit(OpCodes.Ldsfld, presence); + IL.Emit(OpCodes.Ldstr, name); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(typeof(HashSet), "Contains", Types.String)); + IL.Emit(OpCodes.Brtrue, installed); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} before initialization"); + IL.MarkLabel(installed); + IL.Emit(OpCodes.Ldloc, value); IL.Emit(OpCodes.Stsfld, field); IL.Emit(OpCodes.Ldloc, value); + } + SetStackUnknown(); + return true; + } + + private void EmitOrdinaryStaticPrivateBrandCheck(string owner, LocalBuilder receiver, string name) + { + Ctx.ClassRegistry!.TryGetClass(owner, out var builder); + var invalid = IL.DefineLabel(); + var valid = IL.DefineLabel(); + IL.Emit(OpCodes.Ldloc, receiver); + if (builder!.IsGenericTypeDefinition) + { + var constructor = IL.DeclareLocal(Types.Type); + IL.Emit(OpCodes.Isinst, Types.Type); + IL.Emit(OpCodes.Stloc, constructor); + IL.Emit(OpCodes.Ldloc, constructor); IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldloc, constructor); + IL.Emit(OpCodes.Callvirt, Types.GetProperty(Types.Type, "IsGenericType").GetGetMethod()!); + IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldloc, constructor); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.Type, "GetGenericTypeDefinition")); + } + IL.Emit(OpCodes.Ldtoken, builder); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Ceq); IL.Emit(OpCodes.Brtrue, valid); + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} from an object whose class did not declare it"); + IL.MarkLabel(valid); + } + + protected bool TryEmitDefinitionPrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) + { + if (Ctx.CurrentClassName is not { } owner || Ctx.CurrentClassExpr is not { } expression) return false; + string name = token.Lexeme.TrimStart('#'); + var declaration = Ctx.ClassRegistry?.GetPrivateElements(owner); + if (declaration == null) return false; + bool isStatic = declaration.StaticFields.ContainsKey(name); + if (!isStatic && !declaration.FieldNames.Contains(name)) return false; + var receiver = SpillGenericPrivateOperand(receiverExpression); + var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); + var definition = EmitDefinitionPrivateBrandCheck(receiver, name, isStatic); + if (isStatic) + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.PrivateMembers); + if (value != null) + { + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + var installed = IL.DefineLabel(); + IL.Emit(OpCodes.Stloc, slots); + IL.Emit(OpCodes.Ldloc, slots); + IL.Emit(OpCodes.Ldstr, $"field:{name}"); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "ContainsKey", Types.String)); + IL.Emit(OpCodes.Brtrue, installed); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} before initialization"); + IL.MarkLabel(installed); + IL.Emit(OpCodes.Ldloc, slots); + } + } + else if (declaration.InstanceBridge is { } bridge) + IL.Emit(OpCodes.Ldloc, EmitGenericPrivateBrandCheck(bridge, receiver, name)); + else + { + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + var storage = declaration.Storage!; + var template = Ctx.ClassExprFactories![expression].Template; + IL.Emit(OpCodes.Ldsfld, template.IsGenericType ? EmitterTypeHelpers.ResolveField(template, storage) : storage); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Ldloca, slots); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(storage.FieldType, "TryGetValue", Types.Object, Types.DictionaryStringObject.MakeByRefType())); + var valid = IL.DefineLabel(); + IL.Emit(OpCodes.Brtrue, valid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} before initialization"); + IL.MarkLabel(valid); + IL.Emit(OpCodes.Ldloc, slots); + } + IL.Emit(OpCodes.Ldstr, isStatic ? $"field:{name}" : name); + if (value == null) IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "get_Item", Types.String)); + else + { + IL.Emit(OpCodes.Ldloc, value); + IL.Emit(OpCodes.Callvirt, Types.DictionaryStringObjectSetItem); + IL.Emit(OpCodes.Ldloc, value); + } + SetStackUnknown(); + return true; + } + + protected bool TryEmitPrivateMethodValue(Expr.GetPrivate get) + { + if (Ctx.CurrentClassName is not { } current) return false; + string name = get.Name.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateMethodOwner(current, name); + var value = Ctx.ClassRegistry?.GetPrivateMethodValue(owner, name); + if (value is null) return false; + var receiver = SpillGenericPrivateOperand(get.Object); + EmitPrivateMethodValue(value, owner, name, receiver); + return true; + } + + protected bool TryEmitDefinitionPrivateCall(Expr.CallPrivate call) + { + if (Ctx.CurrentClassName is not { } current) return false; + string name = call.Name.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateMethodOwner(current, name); + var value = Ctx.ClassRegistry?.GetPrivateMethodValue(owner, name); + if (value?.Definition == null) return false; + var receiver = SpillGenericPrivateOperand(call.Object); + EmitPrivateMethodValue(value, owner, name, receiver); + EmitGuestMethodInvocation(receiver, call.Arguments); + return true; + } + + private void EmitPrivateMethodValue(PrivateMethodValue value, string owner, string name, LocalBuilder receiver) + { + var definition = value.Definition == null ? null : EmitDefinitionPrivateBrandCheck(receiver, name, value.Source.IsStatic); + if (value.Source.IsStatic && definition == null) + { + EmitOrdinaryStaticPrivateBrandCheck(owner, receiver, name); + } + else if (!value.Source.IsStatic && Ctx.ClassRegistry!.GetPrivateInstanceBridge(owner) is { } bridge) + EmitGenericPrivateBrandCheck(bridge, receiver, name); + else if (!value.Source.IsStatic) + EmitOrdinaryPrivateBrandCheck(owner, receiver, name); + if (definition == null) IL.Emit(OpCodes.Ldsfld, value.Cache!); + else + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.PrivateMembers); + IL.Emit(OpCodes.Ldstr, $"method:{name}"); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "get_Item", Types.String)); + } + SetStackUnknown(); + } + + private LocalBuilder EmitDefinitionPrivateBrandCheck(LocalBuilder receiver, string name, bool isStatic) + { + if (!TryEmitOwnedClassDefinition()) throw new InvalidOperationException("Private access requires its lexical class definition."); + var definition = IL.DeclareLocal(Ctx.Runtime!.ClassDefinitions.Type); + IL.Emit(OpCodes.Stloc, definition); + var invalid = IL.DefineLabel(); + var valid = IL.DefineLabel(); + if (isStatic) IL.Emit(OpCodes.Ldloc, receiver); + else + { + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders!.First(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + var factory = Ctx.ClassExprFactories![expression]; + var field = Ctx.ClassExprDefinitionFields![expression]; + var reference = factory.Template.IsGenericType ? EmitterTypeHelpers.ResolveField(factory.Template, field) : field; + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Isinst, reference.DeclaringType!); + IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Castclass, reference.DeclaringType!); + IL.Emit(OpCodes.Ldfld, reference); + } + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ceq); + IL.Emit(OpCodes.Brtrue, valid); + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} from an object whose class did not declare it"); + IL.MarkLabel(valid); + return definition; + } +} diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.cs index 40001008e..ec74f1463 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.cs @@ -257,6 +257,9 @@ public virtual void EmitExpression(Expr expr) case Expr.GetPrivate gp: EmitGetPrivate(gp); break; + case Expr.PrivateIn presence: + EmitPrivateIn(presence); + break; case Expr.SetPrivate sp: EmitSetPrivate(sp); break; @@ -771,6 +774,7 @@ protected virtual void EmitSetIndex(Expr.SetIndex si) /// protected virtual void EmitThis() { + if (TryEmitGuestThis()) return; var thisField = GetThisField(); if (thisField != null) { @@ -820,6 +824,8 @@ protected virtual void EmitSuper(Expr.Super s) /// protected virtual void EmitVariable(Expr.Variable v) { + if (TryEmitClassDefinitionSelf(v.Name.Lexeme)) return; + if (TryEmitClassDefinitionCapture(v.Name.Lexeme)) return; string name = v.Name.Lexeme; if (TryEmitEnumInitializerMember(name)) @@ -899,6 +905,7 @@ protected virtual void EmitAssign(Expr.Assign a) } IL.Emit(OpCodes.Dup); + if (TryEmitStoreClassDefinitionCapture(name)) { SetStackUnknown(); return; } if (TryEmitGlobalStore(name)) return; Resolver.TryStoreVariable(name); @@ -953,6 +960,7 @@ protected bool TryEmitBuiltInModulePropertySet(Expr.Set s) protected virtual void EmitSet(Expr.Set s) { + if (TryEmitGuestThisSet(s)) return; // CommonJS: `module.exports = X` → stsfld $exports. if (TryEmitCjsSet(s)) return; @@ -975,9 +983,9 @@ protected virtual void EmitSet(Expr.Set s) // Handle static field assignment: Class.field = value if (s.Object is Expr.Variable classVar && - Ctx.Classes.TryGetValue(Ctx.ResolveClassName(classVar.Name.Lexeme), out var staticSetClassBuilder)) + Ctx.Classes.TryGetValue(Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme), out var staticSetClassBuilder)) { - string resolvedClassName = Ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (Ctx.ClassRegistry!.TryGetOwnCallableStaticField(resolvedClassName, s.Name.Lexeme, staticSetClassBuilder, out var staticField)) { EmitExpression(s.Value); @@ -1017,6 +1025,10 @@ protected virtual void EmitSet(Expr.Set s) /// protected virtual void EmitGetPrivate(Expr.GetPrivate gp) { + if (TryEmitPrivateMethodValue(gp)) return; + if (TryEmitDefinitionPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestStaticPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestInstancePrivateField(gp.Object, gp.Name)) return; if (TryEmitGenericPrivateGet(gp)) return; string fieldName = gp.Name.Lexeme; @@ -1085,6 +1097,9 @@ protected virtual void EmitGetPrivate(Expr.GetPrivate gp) /// protected virtual void EmitSetPrivate(Expr.SetPrivate sp) { + if (TryEmitDefinitionPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestStaticPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestInstancePrivateField(sp.Object, sp.Name, sp.Value)) return; if (TryEmitGenericPrivateSet(sp)) return; string fieldName = sp.Name.Lexeme; @@ -1165,6 +1180,7 @@ protected virtual void EmitSetPrivate(Expr.SetPrivate sp) /// protected virtual void EmitCallPrivate(Expr.CallPrivate cp) { + if (TryEmitDefinitionPrivateCall(cp)) return; if (TryEmitGenericPrivateCall(cp)) return; string methodName = cp.Name.Lexeme; @@ -1653,6 +1669,15 @@ protected virtual void EmitNew(Expr.New n) /// protected virtual void EmitUserClassNew(List namespaceParts, string className, Expr.New n) { + if (n.Callee is Expr.Variable variable && Ctx.VarToClassExpr?.ContainsKey(variable.Name.Lexeme) == true) + { + EmitExpression(n.Callee); + EnsureBoxed(); + EmitArgsArrayWithSpread(n.Arguments); + IL.Emit(OpCodes.Call, Ctx.Runtime!.DynamicConstruction.Value); + SetStackUnknown(); + return; + } string resolvedClassName = ResolveClassNameForNew(namespaceParts, className); // Check class expression constructors (e.g., const C = class { }; new C()) @@ -1928,7 +1953,7 @@ protected static string PivotCaptureSource( && names.TryGetValue(capturedVar, out var storage) ? storage : capturedVar; - private void EmitCapturingArrowViaHooks(Expr.ArrowFunction af, MethodBuilder method, ConstructorBuilder displayCtor) + protected void EmitCapturingArrowViaHooks(Expr.ArrowFunction af, MethodBuilder method, ConstructorBuilder displayCtor) { EmitCapturingArrowDisplayViaHooks(af, displayCtor); Types.EmitLoadMethodInfoViaHandle(IL, method); @@ -1962,13 +1987,19 @@ private void EmitCapturingArrowDisplayViaHooks( // than a by-value snapshot — the write case that was previously rejected (#674). The arrow's // own snapshot fields (populated below) deliberately omit these vars (see the function-DC // skip in CollectAndDefineArrowFunctions), so the two paths don't both materialize them. - if (Ctx.ArrowFunctionDCFields?.TryGetValue(af, out var arrowFunctionDCField) == true && - GetFunctionDCField() is FieldBuilder stateMachineFunctionDC) + if (Ctx.ArrowFunctionDCFields?.TryGetValue(af, out var arrowFunctionDCField) == true) { IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Ldarg_0); - IL.Emit(OpCodes.Ldfld, stateMachineFunctionDC); - IL.Emit(OpCodes.Stfld, arrowFunctionDCField); + if (GetFunctionDCField() is FieldBuilder stateMachineFunctionDC && + stateMachineFunctionDC.FieldType == arrowFunctionDCField.FieldType) + { + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, stateMachineFunctionDC); + IL.Emit(OpCodes.Stfld, arrowFunctionDCField); + } + else if (TryEmitClassDefinitionEnvironment(arrowFunctionDCField.FieldType)) + IL.Emit(OpCodes.Stfld, arrowFunctionDCField); + else IL.Emit(OpCodes.Pop); } if (Ctx.DisplayClassFields?.TryGetValue(af, out var fieldMap) == true) @@ -1994,6 +2025,7 @@ private void EmitCapturingArrowDisplayViaHooks( // stub never populates, so resolving it via the map would snapshot null // (NRE when the arrow dereferences `this`). The real receiver lives in the // builder's dedicated ThisField (set by the instance-method stub). + else if (capturedVar == "this" && TryEmitGuestThis()) { } else if (capturedVar == "this" && GetThisField() is FieldBuilder thisField) { IL.Emit(OpCodes.Ldarg_0); @@ -2008,7 +2040,7 @@ private void EmitCapturingArrowDisplayViaHooks( { IL.Emit(OpCodes.Ldloc, local); } - else if (!TryEmitGlobalVariable(sourceVar)) + else if (!TryEmitClassDefinitionCapture(sourceVar) && !TryEmitGlobalVariable(sourceVar)) { IL.Emit(OpCodes.Ldnull); } @@ -2381,9 +2413,9 @@ protected virtual bool TryEmitGlobalStore(string name) protected virtual bool TryEmitStaticFieldAccess(Expr.Get g) { if (g.Object is Expr.Variable classVar && - Ctx.Classes.TryGetValue(Ctx.ResolveClassName(classVar.Name.Lexeme), out var staticFieldClassBuilder)) + Ctx.Classes.TryGetValue(Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme), out var staticFieldClassBuilder)) { - string resolvedClassName = Ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (Ctx.ClassRegistry!.TryGetCallableStaticField(resolvedClassName, g.Name.Lexeme, staticFieldClassBuilder, out var staticField)) { EmitStaticFieldLoadWithShadow(resolvedClassName, staticFieldClassBuilder, g.Name.Lexeme, staticField!); @@ -2401,6 +2433,7 @@ protected virtual bool TryEmitStaticFieldAccess(Expr.Get g) /// protected bool TryEmitStaticClassMethodValue(Expr.Get g) { + if (Ctx.ClassDefinitionParameterIndex != null || Ctx.ClassDefinitionVariableName != null) return false; if (g.Object is not Expr.This || Ctx.IsInstanceMethod || Ctx.CurrentClassBuilder == null) return false; @@ -2530,7 +2563,7 @@ protected void EmitClassHeritageExpression(Expr? heritage, string? innerClassNam IL.Emit(OpCodes.Pop); } - private void EmitClassHeritageValue(Expr heritage, string? innerClassName) + protected void EmitClassHeritageValue(Expr heritage, string? innerClassName) { switch (heritage) { @@ -2889,6 +2922,7 @@ protected bool TryEmitNamespaceVarGet(Expr.Get g) protected virtual void EmitGet(Expr.Get g) { + if (TryEmitGuestThisGet(g)) return; if (StringEmitter.TryEmitPrimitiveStringLengthGet(this, g)) return; diff --git a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs index 650c9ea79..4274e05a9 100644 --- a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs @@ -541,6 +541,9 @@ private void CollectArrowsFromStmt(Stmt stmt, bool isClassMember = false) _currentEnclosingFunctionName = GetDefinitionContext().GetQualifiedFunctionName(f.Name.Lexeme); _currentEnclosingFunctionStmt = f; } + // Generator methods own invocation storage even inside a class factory. + if (isClassMember && f.IsGenerator) + _currentEnclosingFunctionStmt = f; _currentEnclosingCallable = f; _currentCollectStrict = previousStrict || _currentCollectClassName != null @@ -1053,6 +1056,9 @@ or Stmt.Class case Expr.GetPrivate gp: CollectArrowsFromExpr(gp.Object); break; + case Expr.PrivateIn presence: + CollectArrowsFromExpr(presence.Object); + break; case Expr.SetPrivate sp2: CollectArrowsFromExpr(sp2.Object); CollectArrowsFromExpr(sp2.Value); @@ -1511,6 +1517,7 @@ private void EmitArrowBody(Expr.ArrowFunction arrow, MethodBuilder method, TypeB ctx.CurrentClassName = _async.ArrowEnclosingClassNames.TryGetValue(arrow, out var enclosingClassName) ? enclosingClassName : null; + ctx.HasGuestReceiver = _guestReceiverArrows.Contains(arrow); if (ctx.CurrentClassName != null) ctx.CurrentClassBuilder = _classes.Builders.GetValueOrDefault(ctx.CurrentClassName); ctx.CurrentSuperclassIsAnonymousEmptyClass = _arrowsInAnonymousEmptyDerivedClass.Contains(arrow); diff --git a/src/SharpTS/Compilation/ILCompiler.Async.cs b/src/SharpTS/Compilation/ILCompiler.Async.cs index 1d279af9f..d0644f2f4 100644 --- a/src/SharpTS/Compilation/ILCompiler.Async.cs +++ b/src/SharpTS/Compilation/ILCompiler.Async.cs @@ -1035,6 +1035,9 @@ private void AnalyzeArrowExprForAwaits(Expr expr, ref int awaitCount, ref bool s case Expr.Get g: AnalyzeArrowExprForAwaits(g.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + case Expr.PrivateIn presence: + AnalyzeArrowExprForAwaits(presence.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); + break; case Expr.Set s: AnalyzeArrowExprForAwaits(s.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); AnalyzeArrowExprForAwaits(s.Value, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); @@ -1579,6 +1582,8 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth { // Analyze async function to determine await points and hoisted variables var analysis = _async.Analyzer.Analyze(method); + if (isInstanceMethod && (_classExprs.DefinitionMethods.ContainsKey(methodBuilder) || _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)))) + analysis = analysis with { UsesThis = true }; // Check if method has @lock decorator bool hasLock = HasLockDecorator(method); @@ -1594,7 +1599,8 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth isInstanceMethod: isInstanceMethod, hasAsyncArrows: hasAsyncArrows, hasLock: hasLock, - hoistedParameterTypes: GetStableAsyncParameterFieldTypes(method) + hoistedParameterTypes: GetStableAsyncParameterFieldTypes(method), + hasDynamicThis: !isInstanceMethod && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)) ); RegisterStateMachine( methodBuilder, @@ -1657,6 +1663,7 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth // nested private member access inside the async body resolves under module compilation. ctx.CurrentClassName = currentClassName ?? methodBuilder.DeclaringType?.Name; ctx.CurrentClassBuilder = methodBuilder.DeclaringType as TypeBuilder; + ApplyClassDefinitionStateMachineContext(ctx, methodBuilder, isInstanceMethod); // Entry-point display class for captured top-level variables ApplyCapturedTopLevelVariableAccess(ctx); ctx.ArrowEntryPointDCFields = _closures.ArrowEntryPointDCFields.Count > 0 ? _closures.ArrowEntryPointDCFields : null; @@ -1844,6 +1851,7 @@ private void EmitTopLevelAsyncArrowBodies() // ES2022 Private Class Elements support ctx.CurrentClassName = enclosingClassName; ctx.CurrentClassBuilder = enclosingClassBuilder; + ctx.HasGuestReceiver = _guestReceiverArrows.Contains(arrow); ApplyCapturedTopLevelVariableAccess(ctx); // #1222: lets the shadow-capture check distinguish a #1201-lifted binding // (home = entry-DC field, must stay live) from a block-scoped shadow diff --git a/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs b/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs index 00fcbecda..0d3795595 100644 --- a/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs +++ b/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs @@ -185,6 +185,8 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func { // Analyze async generator function to determine yield/await points and hoisted variables var analysis = _asyncGenerators.Analyzer.Analyze(method); + if (isInstanceMethod && (_classExprs.DefinitionMethods.ContainsKey(methodBuilder) || _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)))) + analysis = analysis with { UsesThis = true }; // Build state machine type. A static async generator method (#778) has no `this`/instance fields, // so it is set up like a free function (isInstanceMethod: false, static stub). Use the @@ -195,7 +197,8 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func $"{methodBuilder.DeclaringType!.Name}_{methodBuilder.Name}", analysis, isInstanceMethod: isInstanceMethod, - runtime: _runtime + runtime: _runtime, + hasDynamicThis: !isInstanceMethod && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)) ); RegisterStateMachine( methodBuilder, @@ -238,6 +241,7 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func // generator threads its QUALIFIED class name so nested private member access resolves — #720). ctx.CurrentClassName = currentClassName ?? methodBuilder.DeclaringType?.Name; ctx.CurrentClassBuilder = methodBuilder.DeclaringType as TypeBuilder; + ApplyClassDefinitionStateMachineContext(ctx, methodBuilder, isInstanceMethod); // Entry-point display class for captured top-level variables ApplyCapturedTopLevelVariableAccess(ctx); // Per-arrow $entryPointDC field map so a capturing arrow nested in this instance async diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs index 199786bbc..94f87dddd 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs @@ -31,6 +31,8 @@ private void DefineSymbolAccessorMethod(TypeBuilder typeBuilder, Stmt.Accessor a MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig; if (accessor.IsStatic) attrs |= MethodAttributes.Static; Type[] paramTypes = isGetter ? [] : [typeof(object)]; + if (accessor.IsStatic && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, typeBuilder))) + paramTypes = [_runtime.ClassDefinitions.Type, .. paramTypes]; var methodBuilder = typeBuilder.DefineMethod( $"$sym_{(isGetter ? "get" : "set")}_{list.Count}", diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index fb8c04604..94a48ef93 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -26,6 +26,9 @@ private void DefineClassExpressionTypes() /// private void DefineClassExpression(Expr.ClassExpr classExpr) { + if (_classExprs.Builders.ContainsKey(classExpr)) return; + if (ResolveClassExpressionParent(classExpr) is { } parent && !ReferenceEquals(parent, classExpr)) + DefineClassExpression(parent); string className = _classExprs.Names[classExpr]; // Create TypeBuilder with appropriate attributes @@ -44,16 +47,16 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) var lexicalCaptures = new HashSet(StringComparer.Ordinal); foreach (var method in classExpr.Methods) lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(method)); + lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(classExpr)); + foreach (var accessor in classExpr.Accessors ?? []) + lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(accessor)); if (classExpr.Name != null) lexicalCaptures.Remove(classExpr.Name.Lexeme); - var captureFields = new Dictionary(StringComparer.Ordinal); - foreach (var capture in lexicalCaptures) - { - captureFields[capture] = typeBuilder.DefineField( - $"$lex_{capture}", _types.Object, - FieldAttributes.Public | FieldAttributes.Static); - } - _classExprs.CaptureFields[classExpr] = captureFields; + if (_classExprs.EnclosingClass.TryGetValue(classExpr, out var lexicalClass) + && _classExprs.Names.ContainsValue(lexicalClass)) + lexicalCaptures.Add(CompilationContext.PrivateOwnerCaptureName); + _classExprs.CaptureSlots[classExpr] = lexicalCaptures.OrderBy(name => name, StringComparer.Ordinal) + .Select((name, index) => (name, index)).ToDictionary(entry => entry.name, entry => entry.index, StringComparer.Ordinal); // Track superclass name for inheritance resolution string? superclassName = Expr.GetSuperclassLeafName(classExpr.SuperclassExpr); @@ -83,9 +86,24 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) _classes.GenericParameters.Declare(typeBuilder, classGenericParams ?? []); + EmitTypeDefinitions.AddInterfaceImplementation(typeBuilder, _runtime.ClassDefinitions.InstanceInterface); + var definitionField = typeBuilder.DefineField("$definition", _types.Object, FieldAttributes.Assembly); + _classExprs.DefinitionFields[classExpr] = definitionField; + var definitionGetter = typeBuilder.DefineMethod("$GetClassDefinition", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + _types.Object, Type.EmptyTypes); + typeBuilder.DefineMethodOverride(definitionGetter, _runtime.ClassDefinitions.GetDefinition); + var definitionIl = definitionGetter.GetILGenerator(); + definitionIl.Emit(OpCodes.Ldarg_0); + definitionIl.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(definitionField)); + definitionIl.Emit(OpCodes.Ret); + // NOW resolve superclass (may use our generic params for type arguments) Type? baseType = null; - if (superclassName != null) + bool runtimeHeritage = classExpr.SuperclassExpr != null && _typeMap.Get(classExpr.SuperclassExpr) is TSTypeInfo.Any + && superclassName is not ("Array" or "Promise") + && !Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(superclassName ?? ""); + if (superclassName != null && !runtimeHeritage) { // Check class declarations first (with module resolution) var resolvedSuperName = GetDefinitionContext().ResolveClassName(superclassName); @@ -182,7 +200,7 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) // parameter) cannot be widened to / narrowed from the object-typed $IHasFields slots // without box/unbox of `T`, which yields unverifiable IL. Like class declarations // (ILCompiler.Classes.cs), these fields live in the `_fields` dictionary instead (#291). - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare)) + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsPrivate && !f.IsDeclare && f.ComputedKey == null)) { bool isGenericField = classGenericParams != null && field.TypeAnnotation != null && @@ -193,7 +211,7 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) } // Define static fields (use object type for compatibility with existing emission code) - foreach (var field in classExpr.Fields.Where(f => f.IsStatic)) + foreach (var field in classExpr.Fields.Where(f => f.IsStatic && !f.IsPrivate)) { var staticField = typeBuilder.DefineField( field.Name.Lexeme, @@ -209,6 +227,8 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) // Store the type builder _classExprs.Builders[classExpr] = typeBuilder; + if (_typeMap.GetClassExprType(classExpr) is { } checkedClass) + _classes.CheckedDeclarationNames[checkedClass.Core.DeclarationId] = className; } /// @@ -346,14 +366,16 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) var ctorBuilder = typeBuilder.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, - ctorParamTypes + [.. ctorParamTypes, _runtime.ClassDefinitions.Type] ); + if (HasRuntimeParent(classExpr)) _usesRuntimeParents = true; if (constructor == null && superclassName == "Array") MarkJsVariadicConstructor(ctorBuilder); _classExprs.Constructors[classExpr] = ctorBuilder; _classes.Constructors[className] = ctorBuilder; RegisterArgumentsCapturingMethod(ctorBuilder, constructor?.Body); DefineClassPrototypeConstructor(typeBuilder); + DefineClassExpressionFactory(classExpr, typeBuilder, ctorBuilder, ctorParamTypes); // Define static methods (computed symbol-keyed methods are handled by // DefineClassExpressionSymbolMethods below, like the class-declaration path). @@ -372,9 +394,10 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) method.Name.Lexeme, MethodAttributes.Public | MethodAttributes.Static, returnType, - paramTypes + [_runtime.ClassDefinitions.Type, .. paramTypes] ); _classExprs.StaticMethods[classExpr][method.Name.Lexeme] = methodBuilder; + methodBuilder.DefineParameter(1, ParameterAttributes.None, "__classDefinition"); RegisterArgumentsCapturingMethod(methodBuilder, method.Body); } @@ -459,7 +482,16 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) } } - DefineDeferredComputedMethodKeyRegistrar(classExpr, typeBuilder, classExpr.Fields); + var keys = _classes.ComputedMembers.GetMethods(typeBuilder) + .Select(entry => (entry.Key, entry.Method.Name.Start)) + .Concat(_classes.ComputedMembers.GetAccessors(typeBuilder) + .Select(entry => (entry.Accessor.ComputedKey!, entry.Accessor.Name.Start))) + .Concat(classExpr.Fields.Where(field => field.ComputedKey != null && !field.IsDeclare) + .Select(field => (field.ComputedKey!, field.Name.Start))) + .OrderBy(entry => entry.Start).Select(entry => entry.Item1).ToArray(); + _classExprs.Factories[classExpr] = _classExprs.Factories[classExpr] with { Keys = keys }; + DefinePrivateClassElements(typeBuilder, className, ClassExpressionDeclaration(classExpr), + _classes.GenericParameters.Require(typeBuilder).ToArray(), classExpr); } /// @@ -484,6 +516,10 @@ private void EmitClassExpressionBody(Expr.ClassExpr classExpr) string className = _classExprs.Names[classExpr]; var fieldsField = _classes.InstanceFieldsField[className]; + if (_usesRuntimeParents && !typeBuilder.IsGenericTypeDefinition && !_classes.ErrorSubclasses.Contains(className) + && typeBuilder.BaseType is not { Name: "$Array" or "$Promise" } + && SupportsReceiverInitialization(ClassExpressionDeclaration(classExpr))) + EmitReceiverInitializer(typeBuilder, ClassExpressionDeclaration(classExpr), classExpr); // Emit the constructor-free prototype path before the .cctor that uses it. EmitClassPrototypeConstructor(typeBuilder); @@ -529,6 +565,7 @@ private void EmitClassExpressionBody(Expr.ClassExpr classExpr) // Emit symbol-keyed computed accessor bodies (#281) EmitClassExpressionSymbolAccessors(classExpr, typeBuilder, fieldsField); + EmitClassExpressionPrivateMethodBodies(classExpr, typeBuilder); // Emit $IHasFields interface method bodies (now that method builders are available) EmitHasFieldsInterfaceMethodBodies(className, classExpr); @@ -552,8 +589,15 @@ private CompilationContext CreateClassExpressionContext( ctx.CurrentClassBuilder = typeBuilder; ctx.EmittingTypeBuilder = typeBuilder; ctx.CurrentClassName = className; - if (_classExprs.EnclosingClass.TryGetValue(classExpr, out var enclosingClassName)) - ctx.EnclosingClassNames = [enclosingClassName]; + List enclosingClasses = []; + var lexicalExpression = classExpr; + while (_classExprs.EnclosingClass.TryGetValue(lexicalExpression, out var enclosingClassName)) + { + enclosingClasses.Add(enclosingClassName); + lexicalExpression = _classExprs.Names.FirstOrDefault(entry => entry.Value == enclosingClassName).Key; + if (lexicalExpression is null) break; + } + ctx.EnclosingClassNames = enclosingClasses; ctx.CurrentSuperclassName = _classExprs.Superclass.GetValueOrDefault(classExpr); ctx.PropertyBackingFields = _typedInterop.PropertyBackingFields; ctx.ClassProperties = _typedInterop.ClassProperties; @@ -593,15 +637,6 @@ private CompilationContext CreateClassExpressionContext( // binding throws ReferenceError at runtime (same omission #300 fixed // for class-declaration accessor bodies). ApplyCapturedTopLevelVariableAccess(ctx, memberBodyExports: true); - if (_classExprs.CaptureFields.TryGetValue(classExpr, out var captureFields) - && captureFields.Count > 0) - { - ctx.TopLevelStaticVars = ctx.TopLevelStaticVars == null - ? new Dictionary(captureFields, StringComparer.Ordinal) - : new Dictionary(ctx.TopLevelStaticVars, StringComparer.Ordinal); - foreach (var (name, field) in captureFields) - ctx.TopLevelStaticVars[name] = field; - } return ctx; } @@ -610,80 +645,16 @@ private CompilationContext CreateClassExpressionContext( /// private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, TypeBuilder typeBuilder) { - bool hasStaticFields = classExpr.Fields.Any(f => f.IsStatic && f.Initializer != null); - bool hasStaticInitializers = classExpr.StaticInitializers?.Count > 0; - - var cctor = typeBuilder.DefineConstructor( - MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, - CallingConventions.Standard, - Type.EmptyTypes - ); - + var cctor = typeBuilder.DefineTypeInitializer(); var il = cctor.GetILGenerator(); - var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null, cctor); - ctx.IsStaticConstructorContext = true; - var emitter = new ILEmitter(ctx); - - EmitClassPrototypeRegistration( - il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); - - if (_classes.DeferredDefinitions.TryGet(typeBuilder, out var deferredDefinition)) - { - il.Emit(OpCodes.Ret); - il = deferredDefinition.Initializer.GetILGenerator(); - ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null, deferredDefinition.Initializer); - ctx.IsStaticConstructorContext = true; - emitter = new ILEmitter(ctx); - } - - // Process StaticInitializers if available (preserves declaration order) - if (hasStaticInitializers) + if (_classes.PrivateElements.Require(_classExprs.Names[classExpr]).Storage is { } privateStorage) { - foreach (var initializer in classExpr.StaticInitializers!) - { - switch (initializer) - { - case Stmt.Field field when field.IsStatic && field.ComputedKey != null: - _classes.DeferredDefinitions.TryGetFieldKey(field, out var computedKey); - EmitComputedStaticFieldInitializer(emitter, il, typeBuilder, field, computedKey); - break; - - case Stmt.Field field when field.IsStatic && field.Initializer != null: - var staticField = _classExprs.StaticFields[classExpr][field.Name.Lexeme]; - emitter.EmitExpression(field.Initializer); - if (staticField.FieldType == typeof(object)) - emitter.EmitBoxIfNeeded(field.Initializer); - il.Emit(OpCodes.Stsfld, staticField); - break; - - case Stmt.StaticBlock block: - foreach (var stmt in block.Body) - emitter.EmitStatement(stmt); - break; - } - } + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(privateStorage.FieldType)); + il.Emit(OpCodes.Stsfld, EmitterTypeHelpers.SelfFieldReference(privateStorage)); } - else - { - // Fallback for backward compatibility (fields only) - foreach (var field in classExpr.Fields.Where(f => f.IsStatic && f.Initializer != null)) - { - var staticField = _classExprs.StaticFields[classExpr][field.Name.Lexeme]; - emitter.EmitExpression(field.Initializer!); - if (staticField.FieldType == typeof(object)) - emitter.EmitBoxIfNeeded(field.Initializer!); - il.Emit(OpCodes.Stsfld, staticField); - } - } - - // Register symbol-keyed computed accessors (#281) and methods (#755) in the runtime registry, - // keyed by this class's Type so dynamic bracket get / for...of dispatch can find them. - EmitSymbolAccessorRegistrations(emitter, il, typeBuilder); - EmitSymbolMethodRegistrations(emitter, il, typeBuilder); - + EmitClassPrototypeRegistration(il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); il.Emit(OpCodes.Ret); - if (deferredDefinition != null) - _classes.DeferredDefinitions.MarkInitializerEmitted(deferredDefinition); + EmitClassDefinitionInitializer(classExpr, typeBuilder); } /// @@ -696,6 +667,18 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde var constructor = classExpr.Methods.FirstOrDefault(m => !m.IsStatic && m.Name.Lexeme == "constructor" && m.Body != null); var il = ctorBuilder.GetILGenerator(); + if (HasRuntimeParent(classExpr)) + { + // The definition factory allocates through the bookkeeping constructor + // and uses the explicit-receiver adapter for guest initialization. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.ObjectDefaultCtor); + il.Emit(OpCodes.Ret); + return; + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg, ctorBuilder.GetParameters().Length); + il.Emit(OpCodes.Stfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, fieldsField, ctorBuilder); ctx.IsInstanceMethod = true; @@ -780,7 +763,17 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde else if (ResolveClassExprImplicitBaseConstructor(classExpr) is { } baseCtor) { foreach (var p in baseCtor.GetParameters()) - emitter.EmitOmittedArgument(p.ParameterType); + { + if (p.ParameterType == _runtime.ClassDefinitions.Type) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Parent); + il.Emit(OpCodes.Isinst, _runtime.ClassDefinitions.Type); + } + else emitter.EmitOmittedArgument(p.ParameterType); + } il.Emit(OpCodes.Call, baseCtor); } else @@ -800,7 +793,7 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde ctx.DefineParameter(constructor.Parameters[i].Name.Lexeme, i + 1, paramType); } - var constructorParamTypes = ctorBuilder.GetParameters().Select(p => p.ParameterType).ToArray(); + var constructorParamTypes = ctorBuilder.GetParameters().SkipLast(1).Select(p => p.ParameterType).ToArray(); EmitFunctionEnvironmentPrologue( il, ctx, @@ -841,12 +834,26 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde void EmitInstanceFieldInitializers() { - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && (f.Initializer != null || f.ComputedKey != null))) + var privateStorage = EmitPrivateFieldStorage(il, className); + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && (f.IsPrivate || f.Initializer != null || f.ComputedKey != null))) { + if (field.IsPrivate) + { + EmitPrivateFieldInitializer(il, field, privateStorage!, emitter); + continue; + } if (field.ComputedKey != null) { il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldsfld, _classes.DeferredDefinitions.RequireFieldKey(field)); + var keys = _classExprs.Factories[classExpr].Keys; + int keyIndex = Enumerable.Range(0, keys.Count) + .First(index => ReferenceEquals(keys[index], field.ComputedKey)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Keys); + il.Emit(OpCodes.Ldc_I4, keyIndex); + il.Emit(OpCodes.Ldelem_Ref); if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); @@ -945,8 +952,11 @@ void EmitInstanceFieldInitializers() return _classExprs.ToDefine.FirstOrDefault(candidate => _typeMap.GetClassExprType(candidate)?.Core.DeclarationId == parentId); - // Untyped heritage retains the existing fallback until its runtime value - // can be represented as a CLR base; checked declarations never use it. + if (classExpr.SuperclassExpr != null && _typeMap.Get(classExpr.SuperclassExpr) is TSTypeInfo.Any) + return null; + + // Only checked heritage can select a CLR base. Untyped values are retained + // in the guest definition and dispatched through its runtime parent. return _classExprs.Superclass.GetValueOrDefault(classExpr) is { } name ? _classExprs.VarToClassExpr.GetValueOrDefault(name) : null; } @@ -976,6 +986,7 @@ private static bool ContainsSuperCallInExpr(Expr expr) { Expr.Call call => call.Callee is Expr.Super, Expr.Binary bin => ContainsSuperCallInExpr(bin.Left) || ContainsSuperCallInExpr(bin.Right), + Expr.PrivateIn presence => ContainsSuperCallInExpr(presence.Object), Expr.Logical log => ContainsSuperCallInExpr(log.Left) || ContainsSuperCallInExpr(log.Right), Expr.Grouping grp => ContainsSuperCallInExpr(grp.Expression), _ => false @@ -1090,11 +1101,20 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. var typeBuilder = _classExprs.Builders[classExpr]; + var loweredMethod = method with { Parameters = [new Stmt.Parameter( + new Token(TokenType.IDENTIFIER, "__classDefinition", null, method.Name.Line), "any"), .. method.Parameters] }; + if (_generatorMethodFunctionDCKeys.TryGetValue(method, out var generatorKey)) + _generatorMethodFunctionDCKeys[loweredMethod] = generatorKey; + if (_asyncGeneratorMethodFunctionDCKeys.TryGetValue(method, out var asyncGeneratorKey)) + _asyncGeneratorMethodFunctionDCKeys[loweredMethod] = asyncGeneratorKey; + if (_asyncMethodFunctionDCKeys.TryGetValue(method, out var asyncKey)) + _asyncMethodFunctionDCKeys[loweredMethod] = asyncKey; + // Static generator methods route like a free function (no `this`), mirroring the class- // declaration static path (#765/#778). Async generator FIRST since it has both flags set. if (method.IsAsync && method.IsGenerator) { - EmitAsyncGeneratorMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false); + EmitAsyncGeneratorMethodBody(methodBuilder, loweredMethod, fieldsField: null, isInstanceMethod: false); return; } @@ -1105,19 +1125,20 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. // EmitStaticAsyncMethodBody, which re-resolves from the declaration registry. if (method.IsAsync) { - EmitAsyncMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false, + EmitAsyncMethodBody(methodBuilder, loweredMethod, fieldsField: null, isInstanceMethod: false, currentClassName: _classExprs.Names[classExpr]); return; } if (method.IsGenerator) { - EmitGeneratorMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false); + EmitGeneratorMethodBody(methodBuilder, loweredMethod, fieldsField: null, isInstanceMethod: false); return; } var il = methodBuilder.GetILGenerator(); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null, methodBuilder); + ctx.ClassDefinitionParameterIndex = 0; ctx.IsInstanceMethod = false; SetupSyncMethodFunctionDisplayClass(ctx, il, method); @@ -1125,8 +1146,8 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. var methodParams = methodBuilder.GetParameters(); for (int i = 0; i < method.Parameters.Count; i++) { - Type? paramType = i < methodParams.Length ? methodParams[i].ParameterType : null; - ctx.DefineParameter(method.Parameters[i].Name.Lexeme, i, paramType); + Type? paramType = i + 1 < methodParams.Length ? methodParams[i + 1].ParameterType : null; + ctx.DefineParameter(method.Parameters[i].Name.Lexeme, i + 1, paramType); } var emitter = new ILEmitter(ctx); @@ -1137,10 +1158,10 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. emitter, method.Parameters, method.Body, - environmentParamTypes, - argumentOffset: 0); + environmentParamTypes.Skip(1).ToArray(), + argumentOffset: 1); InitializeSyncMethodCapturedParameters( - ctx, il, method, methodBuilder, argumentOffset: 0); + ctx, il, method, methodBuilder, argumentOffset: 1); if (method.Body != null) { @@ -1185,13 +1206,14 @@ private void EmitClassExpressionSymbolAccessors( var il = methodBuilder.GetILGenerator(); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, accessor.IsStatic ? null : fieldsField, methodBuilder); ctx.IsInstanceMethod = !accessor.IsStatic; + if (accessor.IsStatic) ctx.ClassDefinitionParameterIndex = 0; if (accessor.Kind.Type == TokenType.SET && accessor.SetterParam != null) { var accessorParams = methodBuilder.GetParameters(); Type? paramType = accessorParams.Length > 0 ? accessorParams[0].ParameterType : null; // Instance setter: arg0 is `this`, value at index 1. Static: value at index 0. - int paramIndex = accessor.IsStatic ? 0 : 1; + int paramIndex = 1; ctx.DefineParameter(accessor.SetterParam.Name.Lexeme, paramIndex, paramType); } @@ -1252,7 +1274,8 @@ private void DefineClassExpressionSymbolMethods(Expr.ClassExpr classExpr, TypeBu if (method.IsStatic) attrs |= MethodAttributes.Static; - var mb = typeBuilder.DefineMethod(uniqueName, attrs, returnType, paramTypes); + var mb = typeBuilder.DefineMethod(uniqueName, attrs, returnType, + method.IsStatic ? [_runtime.ClassDefinitions.Type, .. paramTypes] : paramTypes); // Register under the unique name so EmitClassExpression(Static)MethodBody resolves the builder. if (method.IsStatic) diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs index 27e0d7383..2501e4cf8 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs @@ -258,109 +258,11 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel } // Emit instance field initializers to backing fields (before constructor body) - // Note: Declare fields are excluded - they have no initialization - var instanceFieldsWithInit = classStmt.Fields.Where(f => - !f.IsStatic && !f.IsPrivate && !f.IsDeclare && - (f.Initializer != null || f.ComputedKey != null)).ToList(); - if (instanceFieldsWithInit.Count > 0) - { - ctx.FieldsField = fieldsField; - ctx.IsInstanceMethod = true; - var initEmitter = new ILEmitter(ctx); - - foreach (var field in instanceFieldsWithInit) - { - // Handle computed property names (e.g., [mySymbol]: string = "value") - if (field.ComputedKey != null) - { - // Computed keys use dynamic SetIndex to support Symbol keys - // Stack: this - il.Emit(OpCodes.Ldarg_0); - // Load the key captured when the class definition was evaluated. - il.Emit(OpCodes.Ldsfld, _classes.DeferredDefinitions.RequireFieldKey(field)); - // Emit initializer value; a field with no initializer is still an own - // property whose value is undefined. - if (field.Initializer != null) - { - initEmitter.EmitExpression(field.Initializer); - initEmitter.EmitBoxIfNeeded(field.Initializer); - } - else - { - il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); - } - // Call Runtime.SetIndex(object, key, value) - il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); - continue; - } - - string fieldName = field.Name.Lexeme; - string pascalName = NamingConventions.ToPascalCase(fieldName); - - // Check if this is a declared property with a backing field (using PascalCase key) - if (_typedInterop.PropertyBackingFields.TryGetValue(className, out var backingFields) && - backingFields.TryGetValue(pascalName, out var backingField)) - { - // Store directly in backing field - il.Emit(OpCodes.Ldarg_0); // this - - // Emit initializer expression - initEmitter.EmitExpression(field.Initializer!); - - // Convert to proper type if needed - Type targetType = _typedInterop.PropertyTypes[className][pascalName]; - EmitTypeConversion(il, initEmitter, field.Initializer!, targetType); - - il.Emit(OpCodes.Stfld, backingField); - } - else - { - // Fallback: store in _extras dictionary (for fields without backing fields) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); - il.Emit(OpCodes.Ldstr, fieldName); - // number[] unboxing: an `arr: number[] = []` field is created as a numeric $Array - // (escaping by nature — a field is aliasable), so `this.arr[i]=v` writes unboxed. - if (!initEmitter.TryEmitNumericEmptyArrayInit(field.TypeAnnotation, field.Initializer)) - { - initEmitter.EmitExpression(field.Initializer!); - initEmitter.EmitBoxIfNeeded(field.Initializer!); - } - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); - } - } - } - - // Initialize instance declare fields (without initializers) to null in _extras dictionary - // TypeScript semantics: uninitialized fields return null/undefined, not CLR defaults - var instanceDeclareFields = classStmt.Fields.Where(f => - !f.IsStatic && !f.IsPrivate && f.IsDeclare && f.Initializer == null && f.ComputedKey == null).ToList(); - foreach (var field in instanceDeclareFields) - { - string fieldName = field.Name.Lexeme; - // Store null in _extras dictionary - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); - il.Emit(OpCodes.Ldstr, fieldName); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); - } - - // A derived class does not install its private brand until super() returns. Deferring - // this prevents a base field initializer that dispatches into the derived instance from - // observing derived private elements prematurely. bool deferPrivateInitialization = constructor?.Body != null && classStmt.SuperclassExpr != null && constructor.Body.Any(ContainsSuperCall); - bool privateInitializationEmitted = false; - if (!deferPrivateInitialization) - { - // ES2022: Initialize instance private fields. Private fields use a - // ConditionalWeakTable for GC-friendly per-instance storage. - EmitPrivateFieldInitialization(il, className, classStmt, ctx); - privateInitializationEmitted = true; - } - + bool privateInitializationEmitted = !deferPrivateInitialization; + if (!deferPrivateInitialization) EmitOrderedFieldInitializers(); // TypeScript 4.9+: Initialize instance auto-accessor backing fields if (classStmt.AutoAccessors != null) { @@ -421,7 +323,7 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel emitter.EmitStatement(stmt); if (!privateInitializationEmitted && ContainsSuperCall(stmt)) { - EmitPrivateFieldInitialization(il, className, classStmt, ctx); + EmitOrderedFieldInitializers(); privateInitializationEmitted = true; } } @@ -431,77 +333,134 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel } il.Emit(OpCodes.Ret); - } - - /// - /// Emits IL to initialize ES2022 private fields for a new instance. - /// Creates a Dictionary with initial values and adds it to the ConditionalWeakTable. - /// - private void EmitPrivateFieldInitialization( - ILGenerator il, - string className, - Stmt.Class classStmt, - CompilationContext ctx) - { - // Check if this class has instance private fields - var privateElements = _classes.PrivateElements.Require(className); - if (privateElements.Storage is not { } storageField) - return; - var fieldNames = privateElements.FieldNames; - - var instancePrivateFields = classStmt.Fields - .Where(f => f.IsPrivate && !f.IsStatic) - .ToList(); + void EmitOrderedFieldInitializers() + { + // Note: Declare fields are excluded - they have no initialization + var instanceFieldsWithInit = classStmt.Fields.Where(f => + !f.IsStatic && !f.IsDeclare && + (f.IsPrivate || f.Initializer != null || f.ComputedKey != null)).ToList(); + if (instanceFieldsWithInit.Count > 0 || _classes.PrivateElements.Require(className).Storage != null) + { + ctx.FieldsField = fieldsField; + ctx.IsInstanceMethod = true; + var initEmitter = new ILEmitter(ctx); - ctx.FieldsField = null; // Not using _fields for private field init - ctx.IsInstanceMethod = true; - var initEmitter = new ILEmitter(ctx); + var privateStorage = EmitPrivateFieldStorage(il, className); + foreach (var field in instanceFieldsWithInit) + { + if (field.IsPrivate) + { + EmitPrivateFieldInitializer(il, field, privateStorage!, initEmitter); + continue; + } + // Handle computed property names (e.g., [mySymbol]: string = "value") + if (field.ComputedKey != null) + { + // Computed keys use dynamic SetIndex to support Symbol keys + // Stack: this + il.Emit(OpCodes.Ldarg_0); + // Load the key captured when the class definition was evaluated. + il.Emit(OpCodes.Ldsfld, _classes.DeferredDefinitions.RequireFieldKey(field)); + // Emit initializer value; a field with no initializer is still an own + // property whose value is undefined. + if (field.Initializer != null) + { + initEmitter.EmitExpression(field.Initializer); + initEmitter.EmitBoxIfNeeded(field.Initializer); + } + else + { + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + } + // Call Runtime.SetIndex(object, key, value) + il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); + continue; + } - // Create local for the fields dictionary - var dictType = typeof(Dictionary); - var dictLocal = il.DeclareLocal(dictType); + string fieldName = field.Name.Lexeme; + string pascalName = NamingConventions.ToPascalCase(fieldName); - // Dictionary __fields = new Dictionary(capacity) - il.Emit(OpCodes.Ldc_I4, fieldNames.Count); - il.Emit(OpCodes.Newobj, dictType.GetConstructor([typeof(int)])!); - il.Emit(OpCodes.Stloc, dictLocal); + // Check if this is a declared property with a backing field (using PascalCase key) + if (_typedInterop.PropertyBackingFields.TryGetValue(className, out var backingFields) && + backingFields.TryGetValue(pascalName, out var backingField)) + { + // Store directly in backing field + il.Emit(OpCodes.Ldarg_0); // this - // Add each private field with its initializer value (or null) - foreach (var field in instancePrivateFields) - { - string fieldName = field.Name.Lexeme; - if (fieldName.StartsWith('#')) - fieldName = fieldName[1..]; + // Emit initializer expression + initEmitter.EmitExpression(field.Initializer!); - // __fields[fieldName] = initializer ?? null - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldstr, fieldName); + // Convert to proper type if needed + Type targetType = _typedInterop.PropertyTypes[className][pascalName]; + EmitTypeConversion(il, initEmitter, field.Initializer!, targetType); - if (field.Initializer != null) - { - initEmitter.EmitExpression(field.Initializer); - initEmitter.EmitBoxIfNeeded(field.Initializer); + il.Emit(OpCodes.Stfld, backingField); + } + else + { + // Fallback: store in _extras dictionary (for fields without backing fields) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); + il.Emit(OpCodes.Ldstr, fieldName); + // number[] unboxing: an `arr: number[] = []` field is created as a numeric $Array + // (escaping by nature — a field is aliasable), so `this.arr[i]=v` writes unboxed. + if (!initEmitter.TryEmitNumericEmptyArrayInit(field.TypeAnnotation, field.Initializer)) + { + initEmitter.EmitExpression(field.Initializer!); + initEmitter.EmitBoxIfNeeded(field.Initializer!); + } + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); + } + } } - else + + // Initialize instance declare fields (without initializers) to null in _extras dictionary + // TypeScript semantics: uninitialized fields return null/undefined, not CLR defaults + var instanceDeclareFields = classStmt.Fields.Where(f => + !f.IsStatic && !f.IsPrivate && f.IsDeclare && f.Initializer == null && f.ComputedKey == null).ToList(); + foreach (var field in instanceDeclareFields) { + string fieldName = field.Name.Lexeme; + // Store null in _extras dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); + il.Emit(OpCodes.Ldstr, fieldName); il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); } - il.Emit(OpCodes.Callvirt, dictType.GetMethod("set_Item", [typeof(string), typeof(object)])!); + } + } + + // Install the method brand before evaluating fields; each field becomes present + // only after its initializer completes. + private LocalBuilder? EmitPrivateFieldStorage(ILGenerator il, string className) + { + var elements = _classes.PrivateElements.Require(className); + if (elements.Storage is not { } storage) return null; + var slots = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Ldc_I4, elements.FieldNames.Count); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.DictionaryStringObject, _types.Int32)); + il.Emit(OpCodes.Stloc, slots); + il.Emit(OpCodes.Ldsfld, EmitterTypeHelpers.SelfFieldReference(storage)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, slots); + il.Emit(OpCodes.Callvirt, _types.GetMethod(storage.FieldType, "Add", typeof(object), _types.DictionaryStringObject)); + return slots; + } - // __privateFields.Add(this, __fields) - var cwtType = EmitGenerics.MakeGenericType(typeof(System.Runtime.CompilerServices.ConditionalWeakTable<,>), typeof(object), typeof(Dictionary)); - var addMethod = _types.GetMethod( - cwtType, - "Add", - typeof(object), - typeof(Dictionary)); - - il.Emit(OpCodes.Ldsfld, storageField); // __privateFields - il.Emit(OpCodes.Ldarg_0); // this - il.Emit(OpCodes.Ldloc, dictLocal); // __fields - il.Emit(OpCodes.Callvirt, addMethod); + private void EmitPrivateFieldInitializer(ILGenerator il, Stmt.Field field, LocalBuilder slots, ILEmitter emitter) + { + il.Emit(OpCodes.Ldloc, slots); + il.Emit(OpCodes.Ldstr, field.Name.Lexeme.TrimStart('#')); + if (field.Initializer is { } initializer) + { + emitter.EmitExpression(initializer); + emitter.EmitBoxIfNeeded(initializer); + } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); } } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs new file mode 100644 index 000000000..d053acced --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs @@ -0,0 +1,216 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private void EmitClassDefinitionInitializer(Expr.ClassExpr expression, TypeBuilder template) + { + var factory = _classExprs.Factories[expression]; + var il = factory.Initializer.GetILGenerator(); + var ctx = CreateClassExpressionContext(il, expression, template, null, factory.Initializer); + ctx.EmittingTypeBuilder = _programType; + ctx.ClassDefinitionParameterIndex = 0; + ctx.ClassDefinitionThisParameterIndex = 0; + var emitter = new ILEmitter(ctx); + var storage = _runtime.DescriptorStorage; + foreach (var value in _classes.PrivateElements.Require(_classExprs.Names[expression]).MethodValues.Values) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.PrivateMembers); + il.Emit(OpCodes.Ldstr, $"method:{value.Source.Name.Lexeme.TrimStart('#')}"); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Newobj, value.Constructor); + EmitMethodInfoLiteral(il, value.Method, value.Type); + il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); + } + + var members = new List<(int Position, bool Static, Expr Key, MethodBuilder Method, bool? Getter, IReadOnlyList Parameters)>(); + foreach (var method in expression.Methods.Where(method => method.Body != null && + !method.IsPrivate && method.Name.Lexeme != "constructor" && method.ComputedKey == null)) + { + var builder = method.IsStatic ? _classExprs.StaticMethods[expression][method.Name.Lexeme] + : _classExprs.InstanceMethods[expression][method.Name.Lexeme]; + members.Add((method.Name.Start, method.IsStatic, new Expr.Literal(method.Name.Lexeme), builder, null, method.Parameters)); + } + foreach (var (method, key, builder) in _classes.ComputedMembers.GetMethods(template)) + members.Add((method.Name.Start, method.IsStatic, key, builder, null, method.Parameters)); + foreach (var (accessor, builder) in _classes.ComputedMembers.GetAccessors(template)) + members.Add((accessor.Name.Start, accessor.IsStatic, accessor.ComputedKey!, builder, accessor.Kind.Type == TokenType.GET, + accessor.SetterParam == null ? [] : [accessor.SetterParam])); + foreach (var accessor in (expression.Accessors ?? []).Where(accessor => !accessor.IsStatic && accessor.ComputedKey == null)) + { + var name = NamingConventions.ToPascalCase(accessor.Name.Lexeme); + var builder = accessor.Kind.Type == TokenType.GET ? _classExprs.Getters[expression][name] : _classExprs.Setters[expression][name]; + members.Add((accessor.Name.Start, false, new Expr.Literal(accessor.Name.Lexeme), builder, accessor.Kind.Type == TokenType.GET, + accessor.SetterParam == null ? [] : [accessor.SetterParam])); + } + foreach (var member in members.OrderBy(member => member.Position)) + Register(member.Static, member.Key, member.Method, member.Getter, member.Parameters); + + foreach (var initializer in expression.StaticInitializers ?? expression.Fields.Where(field => field.IsStatic).Cast().ToList()) + { + if (initializer is Stmt.Field field && field.IsStatic && !field.IsDeclare) + { + if (field.IsPrivate) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.PrivateMembers); + il.Emit(OpCodes.Ldstr, $"field:{field.Name.Lexeme.TrimStart('#')}"); + if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); emitter.EmitBoxIfNeeded(field.Initializer); } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); + continue; + } + il.Emit(OpCodes.Ldarg_0); + EmitKey(field.ComputedKey ?? new Expr.Literal(field.Name.Lexeme)); + if (field.Initializer != null) + { + emitter.EmitExpression(field.Initializer); + emitter.EmitBoxIfNeeded(field.Initializer); + } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + var value = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, value); + var key = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, key); + var owner = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, owner); + var descriptor = NewDescriptor(); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Ldloc, value); + il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetSetMethod()!); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, storage.DescriptorEnumerable.GetSetMethod()!); + StoreDescriptor(owner, key, descriptor); + } + else if (initializer is Stmt.StaticBlock block) + foreach (var statement in block.Body) emitter.EmitStatement(statement); + } + il.Emit(OpCodes.Ret); + + void EmitKey(Expr key) + { + int index = Enumerable.Range(0, factory.Keys.Count).FirstOrDefault(index => ReferenceEquals(factory.Keys[index], key), -1); + if (index >= 0) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Keys); + il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Ldelem_Ref); + } + else if (key is Expr.Literal { Value: string name }) il.Emit(OpCodes.Ldstr, name); + else throw new InvalidOperationException("A computed definition key was not declared."); + } + + LocalBuilder NewDescriptor() + { + var result = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Newobj, storage.DescriptorConstructor); + il.Emit(OpCodes.Stloc, result); + foreach (var property in new[] { storage.DescriptorWritable, storage.DescriptorConfigurable }) + { + il.Emit(OpCodes.Ldloc, result); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, property.GetSetMethod()!); + } + il.Emit(OpCodes.Ldloc, result); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, storage.DescriptorEnumerable.GetSetMethod()!); + return result; + } + + void StoreDescriptor(LocalBuilder owner, LocalBuilder key, LocalBuilder descriptor) + { + var stringKey = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringKey); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Call, _runtime.Symbols.GetStorage); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "set_Item", _types.Object, _types.Object)); + il.Emit(OpCodes.Br, done); + il.MarkLabel(stringKey); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Call, storage.DefineProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(done); + } + + void Register(bool isStatic, Expr keyExpression, MethodBuilder method, bool? getter, IReadOnlyList parameters) + { + var owner = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + if (!isStatic) il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Prototype); + il.Emit(OpCodes.Stloc, owner); + var key = il.DeclareLocal(_types.Object); + EmitKey(keyExpression); + il.Emit(OpCodes.Stloc, key); + var descriptor = NewDescriptor(); + if (getter.HasValue) + { + // Preserve the other half when getter and setter share a key. + var noExisting = il.DefineLabel(); + var haveExisting = il.DefineLabel(); + var existing = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, noExisting); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, existing); + il.Emit(OpCodes.Br, haveExisting); + il.MarkLabel(noExisting); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Call, _runtime.Symbols.GetStorage); + il.Emit(OpCodes.Ldloc, key); + var found = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloca, found); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, found); + il.Emit(OpCodes.Isinst, storage.DescriptorType); + il.Emit(OpCodes.Stloc, existing); + il.MarkLabel(haveExisting); + var fresh = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, existing); + il.Emit(OpCodes.Brfalse, fresh); + il.Emit(OpCodes.Ldloc, existing); + il.Emit(OpCodes.Stloc, descriptor); + il.MarkLabel(fresh); + } + il.Emit(OpCodes.Ldloc, descriptor); + if (isStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, factory.Template.IsGenericType ? EmitterTypeHelpers.ResolveMethod(factory.Template, method) : method); + il.Emit(OpCodes.Ldtoken, factory.Template); + il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + } + else + { + var value = DefineDefinitionMethodValue(expression, method, parameters); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Newobj, value.Constructor); + EmitMethodInfoLiteral(il, value.Method, value.Type); + } + il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); + il.Emit(OpCodes.Callvirt, (getter == true ? storage.DescriptorGetter : getter == false ? storage.DescriptorSetter : storage.DescriptorValue).GetSetMethod()!); + StoreDescriptor(owner, key, descriptor); + } + } + +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs new file mode 100644 index 000000000..84a48619a --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private (TypeBuilder Type, ConstructorBuilder Constructor, MethodBuilder Method) DefineDefinitionMethodValue( + Expr.ClassExpr expression, MethodBuilder original, IReadOnlyList parameters) + { + var type = EmitTypeDefinitions.DefineType(_moduleBuilder, + $"$DefinitionMember_{_classExprs.Names[expression]}_{original.Name}_{(original.IsStatic ? "Static" : "Instance")}", TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + var field = type.DefineField("Definition", _runtime.ClassDefinitions.Type, FieldAttributes.Assembly | FieldAttributes.InitOnly); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [_runtime.ClassDefinitions.Type]); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, field); + il.Emit(OpCodes.Ret); + var method = type.DefineMethod("Invoke", MethodAttributes.Public, original.ReturnType, + [_types.Object, .. parameters.Select(parameter => parameter.IsRest ? _types.ListOfObject : _types.Object)]); + method.DefineParameter(1, ParameterAttributes.None, "__this"); + MarkExpectsThis(method); + MarkNonConstructible(method); + MarkPadsUndefined(method); + MarkFunctionLength(method, parameters); + MarkFunctionName(method, original.Name); + _classExprs.DefinitionMethods.Add(method, (expression, field)); + + Stmt.Function? source = expression.Methods.FirstOrDefault(source => + source.ComputedKey == null && source.IsStatic == original.IsStatic && source.Name.Lexeme == original.Name); + source ??= _classes.ComputedMembers.GetMethods(_classExprs.Builders[expression]) + .FirstOrDefault(entry => ReferenceEquals(entry.Builder, original)).Method; + if (source == null) + { + var accessor = _classes.ComputedMembers.GetAccessors(_classExprs.Builders[expression]) + .FirstOrDefault(entry => ReferenceEquals(entry.Method, original)).Accessor; + accessor ??= (expression.Accessors ?? []).First(accessor => + original.Name == $"{(accessor.Kind.Type == TokenType.GET ? "get" : "set")}_{NamingConventions.ToPascalCase(accessor.Name.Lexeme)}"); + source = new Stmt.Function(accessor.Name, null, null, parameters.ToList(), accessor.Body, null, IsStatic: accessor.IsStatic); + } + EmitGuestMethodValueBody(method, source, () => + { + var ctx = CreateClassExpressionContext(method.GetILGenerator(), expression, _classExprs.Builders[expression], null, method); + ctx.EmittingTypeBuilder = type; + ctx.IsInstanceMethod = !source.IsStatic; + ctx.ClassDefinitionOwnerField = field; + ctx.GuestThisVariableName = "__this"; + ctx.HasGuestReceiver = true; + foreach (var parameter in expression.TypeParams ?? []) ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; + return ctx; + }, _classExprs.Names[expression]); + type.CreateType(); + return (type, constructor, method); + } + +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs new file mode 100644 index 000000000..4722f6334 --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs @@ -0,0 +1,85 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private void DefineClassExpressionFactory(Expr.ClassExpr expression, TypeBuilder builder, + ConstructorBuilder constructor, Type[] parameterTypes) + { + Type template = builder; + ConstructorInfo target = constructor; + if (builder.IsGenericTypeDefinition) + { + var erased = expression.TypeParams!.Select(parameter => parameter.Constraint is { } constraint + ? ResolveConstraintType(constraint) : _types.Object).ToArray(); + template = EmitGenerics.MakeGenericType(builder, erased); + target = EmitterTypeHelpers.ResolveConstructor(template, constructor); + } + var method = _programType.DefineMethod("$Create_" + _classExprs.Names[expression], + MethodAttributes.Public | MethodAttributes.Static, _types.Object, [_types.ObjectArray, _types.Object]); + var il = method.GetILGenerator(); + if (HasRuntimeParent(expression)) + { + var receiver = il.DeclareLocal(template); + ConstructorInfo allocation = _classes.PrototypeConstructors[builder]; + if (template != builder) allocation = EmitterTypeHelpers.ResolveConstructor(template, allocation); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Newobj, allocation); + il.Emit(OpCodes.Stloc, receiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldarg_1); + FieldInfo definitionField = _classExprs.DefinitionFields[expression]; + if (template != builder) definitionField = EmitterTypeHelpers.ResolveField(template, definitionField); + il.Emit(OpCodes.Stfld, definitionField); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.InitializeReceiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ret); + } + else + { + if (_functions.MethodsCapturingArguments.Contains(constructor)) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stsfld, _runtime.Arguments.CurrentField); + } + for (int i = 0; i < parameterTypes.Length; i++) + { + if (parameterTypes[i] == _types.ObjectArray) + { + il.Emit(OpCodes.Ldarg_0); + continue; + } + var missing = il.DefineLabel(); + var ready = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ble, missing); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Br, ready); + il.MarkLabel(missing); + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.MarkLabel(ready); + } + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Newobj, target); + il.Emit(OpCodes.Ret); + } + string name = expression.Name?.Lexeme + ?? _classExprs.VarToClassExpr.FirstOrDefault(entry => ReferenceEquals(entry.Value, expression)).Key + ?? ""; + var initializer = _programType.DefineMethod("$Initialize_" + _classExprs.Names[expression], + MethodAttributes.Assembly | MethodAttributes.Static, _types.Void, [_runtime.ClassDefinitions.Type]); + _classExprs.Factories[expression] = new(method, template, name, GetClassConstructorLength(expression.Methods), initializer, []); + } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs index 5033061ff..410e6d883 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs @@ -1004,14 +1004,29 @@ private void EmitClassPrototypeFieldFallback(ILGenerator il) var skipLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Call, _runtime.ClassPrototypes.Get); + il.Emit(OpCodes.Call, _runtime.ObjectPrototypes.GetPrototypeOf); il.Emit(OpCodes.Stloc, prototypeLocal); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldloc, prototypeLocal); il.Emit(OpCodes.Ceq); il.Emit(OpCodes.Brtrue, skipLabel); + var noDescriptor = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _runtime.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Brfalse, noDescriptor); + var inherited = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, prototypeLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.ReadPrototypeProperty); + il.Emit(OpCodes.Stloc, inherited); + il.Emit(OpCodes.Ldloc, inherited); + il.Emit(OpCodes.Isinst, _runtime.Sentinels.UndefinedType); + il.Emit(OpCodes.Brtrue, noDescriptor); + il.Emit(OpCodes.Ldloc, inherited); + il.Emit(OpCodes.Ret); + il.MarkLabel(noDescriptor); il.Emit(OpCodes.Ldloc, prototypeLocal); il.Emit(OpCodes.Isinst, _runtime.ObjectFields.Interface); il.Emit(OpCodes.Brfalse, skipLabel); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs index 26669a9c9..c84ba9b3a 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs @@ -30,6 +30,8 @@ private void EmitAllHasFieldsInterfaceMethodBodies(IEnumerable statements) { foreach (var classStmt in CollectClassDeclarations(statements)) { + if (_runtimeClassDeclarations.ContainsKey(classStmt)) continue; + using var namespaceScope = new ClassNamespaceScope(this, classStmt); if (classStmt.IsDeclare) continue; @@ -50,6 +52,7 @@ private void EmitAllHasFieldsInterfaceMethodBodies(IEnumerable statements) /// private void DefineClassMethodsOnly(Stmt.Class classStmt) { + using var namespaceScope = new ClassNamespaceScope(this, classStmt); // Skip @DotNetType external type classes - they don't have TypeBuilders if (classStmt.IsDeclare) return; @@ -84,7 +87,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) if (constructor != null) { ctorParamTypes = ParameterTypeResolver.ResolveConstructorParameters( - classStmt.Name.Lexeme, constructor.Parameters, _typeMapper, _typeMap); + qualifiedClassName, constructor.Parameters, _typeMapper, _typeMap); } else if (classStmt.SuperclassExpr != null) { @@ -172,7 +175,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) // Use typed parameters from TypeMap var paramTypes = ParameterTypeResolver.ResolveMethodParameters( - classStmt.Name.Lexeme, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); + qualifiedClassName, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); // Set return type based on method kind // Must check async generator FIRST since it has both IsAsync and IsGenerator true var returnType = (method.IsAsync && method.IsGenerator) ? _types.IAsyncEnumerableOfObject : @@ -201,7 +204,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) // Use typed parameters from TypeMap var paramTypes = ParameterTypeResolver.ResolveMethodParameters( - classStmt.Name.Lexeme, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); + qualifiedClassName, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); MethodAttributes methodAttrs = MethodAttributes.Public | MethodAttributes.Virtual; if (method.IsAbstract) @@ -546,6 +549,7 @@ private void CreateExplicitAccessorProperties(TypeBuilder typeBuilder, string cl private void EmitClassMethods(Stmt.Class classStmt) { + using var namespaceScope = new ClassNamespaceScope(this, classStmt); if (!_classes.EmittedMethodBodies.Add(classStmt)) return; @@ -585,6 +589,9 @@ private void EmitClassMethods(Stmt.Class classStmt) // Emit constructor EmitConstructor(typeBuilder, classStmt, fieldsField); + if (_usesRuntimeParents && !typeBuilder.IsGenericTypeDefinition && typeBuilder.BaseType is not { Name: "$Array" or "$Promise" } + && !_classes.ErrorSubclasses.Contains(qualifiedClassName) && SupportsReceiverInitialization(classStmt)) + EmitReceiverInitializer(typeBuilder, classStmt); // Emit the compiler-only constructor used to create Constructor.prototype // without running JavaScript constructor bodies or field initializers. @@ -777,6 +784,7 @@ private void EmitPrivateMethodBodies(TypeBuilder typeBuilder, Stmt.Class classSt throw new InvalidOperationException($"Private method '{qualifiedClassName}.{methodName}' has not been declared."); EmitPrivateMethodBody(typeBuilder, methodBuilder, method, fieldsField, qualifiedClassName, method.IsStatic); } + EmitPrivateMethodValueBodies(qualifiedClassName); _classes.PrivateElements.MarkBodiesEmitted(qualifiedClassName); } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs new file mode 100644 index 000000000..09c294917 --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs @@ -0,0 +1,183 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; +using SharpTS.Parsing.Visitors; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private Stmt.Class ClassExpressionDeclaration(Expr.ClassExpr expression) => new( + new Token(TokenType.IDENTIFIER, _classExprs.Names[expression], null, 0), expression.TypeParams, + expression.SuperclassExpr, expression.SuperclassTypeArgs, expression.Methods, expression.Fields, + expression.Accessors, StaticInitializers: expression.StaticInitializers); + + private void EmitClassExpressionPrivateMethodBodies(Expr.ClassExpr expression, TypeBuilder template) + { + string name = _classExprs.Names[expression]; + var declaration = _classes.PrivateElements.Require(name); + foreach (var value in declaration.MethodValues.Values) + { + var original = (value.Source.IsStatic ? declaration.StaticMethods : declaration.Methods)[value.Source.Name.Lexeme.TrimStart('#')]; + var il = original.GetILGenerator(); + if (value.Source.IsStatic) il.Emit(OpCodes.Ldarg_0); + else + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[expression])); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + } + var definition = il.DeclareLocal(_runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Stloc, definition); + if (value.Source.IsStatic) il.Emit(OpCodes.Ldloc, definition); + else il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.PrivateMembers); + il.Emit(OpCodes.Ldstr, $"method:{value.Source.Name.Lexeme.TrimStart('#')}"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "get_Item", _types.String)); + il.Emit(OpCodes.Ldc_I4, value.Source.Parameters.Count); + il.Emit(OpCodes.Newarr, _types.Object); + for (int i = 0; i < value.Source.Parameters.Count; i++) + { + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4, i); il.Emit(OpCodes.Ldarg, i + 1); il.Emit(OpCodes.Stelem_Ref); + } + il.Emit(OpCodes.Call, _runtime.Invocation.Method); + if (original.ReturnType != _types.Object) il.Emit(OpCodes.Castclass, original.ReturnType); + il.Emit(OpCodes.Ret); + } + EmitPrivateMethodValueBodies(name); + _classes.PrivateElements.MarkBodiesEmitted(name); + } + + private readonly Dictionary _privateMethodValueOwners = []; + private readonly HashSet _guestReceiverArrows = new(ReferenceEqualityComparer.Instance); + + private sealed class GuestReceiverArrowCollector(HashSet arrows) : AstVisitorBase + { + protected override void VisitArrowFunction(Expr.ArrowFunction expression) + { + if (expression.HasOwnThis) return; + arrows.Add(expression); + base.VisitArrowFunction(expression); + } + protected override void VisitClassExpr(Expr.ClassExpr expression) { } + protected override void VisitFunction(Stmt.Function statement) { } + } + + private void DefinePrivateMethodValues(string className, TypeBuilder owner, Stmt.Class declaration, Expr.ClassExpr? expression = null) + { + foreach (var source in declaration.Methods.Where(method => method.IsPrivate && method.Body != null)) + { + var collector = new GuestReceiverArrowCollector(_guestReceiverArrows); + foreach (var statement in source.Body!) collector.Visit(statement); + string name = source.Name.Lexeme.TrimStart('#'); + var type = EmitTypeDefinitions.DefineType(_moduleBuilder, $"$PrivateMember_{className}_{name}", + TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + var definition = expression == null ? null : type.DefineField("Definition", _runtime.ClassDefinitions.Type, FieldAttributes.Assembly | FieldAttributes.InitOnly); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + expression == null ? Type.EmptyTypes : [_runtime.ClassDefinitions.Type]); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + if (definition != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, definition); + } + il.Emit(OpCodes.Ret); + var method = type.DefineMethod("Invoke", MethodAttributes.Public, + ResolvePrivateMethodReturnType(source, source.IsStatic), + [_types.Object, .. source.Parameters.Select(parameter => parameter.IsRest ? typeof(List) : _types.Object)]); + method.DefineParameter(1, ParameterAttributes.None, "__this"); + MarkExpectsThis(method); + MarkNonConstructible(method); + MarkPadsUndefined(method); + MarkFunctionLength(method, source.Parameters); + MarkFunctionName(method, $"#{name}"); + FieldBuilder? cache = null; + ConstructorBuilder? initializer = null; + if (expression == null) + { + cache = type.DefineField("Value", _types.Object, FieldAttributes.Assembly | FieldAttributes.Static | FieldAttributes.InitOnly); + initializer = type.DefineTypeInitializer(); + il = initializer.GetILGenerator(); + il.Emit(OpCodes.Newobj, constructor); + EmitMethodInfoLiteral(il, method, type); + il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); + il.Emit(OpCodes.Stsfld, cache); + il.Emit(OpCodes.Ret); + } + else _classExprs.DefinitionMethods.Add(method, (expression, definition!)); + _classes.PrivateElements.DeclareMethodValue(className, name, + new PrivateMethodValue(type, constructor, method, cache, initializer, source, definition)); + _privateMethodValueOwners.Add(method, (className, owner, declaration)); + } + } + + private void EmitPrivateMethodValueBodies(string className) + { + foreach (var value in _classes.PrivateElements.Require(className).MethodValues.Values) + { + var owner = _privateMethodValueOwners[value.Method]; + EmitGuestMethodValueBody(value.Method, value.Source, () => + { + var ctx = _classExprs.DefinitionMethods.TryGetValue(value.Method, out var evaluated) + ? CreateClassExpressionContext(value.Method.GetILGenerator(), evaluated.Expression, owner.Owner, null, value.Method) + : CreateModuleMemberContext(value.Method.GetILGenerator(), value.Method); + ctx.ClassDefinitionOwnerField = value.Definition; + ApplyPrivateMethodValueContext(ctx, value.Method); + ctx.EmittingTypeBuilder = value.Type; + ctx.ArrowEntryPointDCFields = _closures.ArrowEntryPointDCFields.Count > 0 ? _closures.ArrowEntryPointDCFields : null; + ctx.ArrowFunctionDCFields = _closures.ArrowFunctionDCFields.Count > 0 ? _closures.ArrowFunctionDCFields : null; + ctx.ArrowScopeDCFields = _closures.ArrowScopeDCFields.Count > 0 ? _closures.ArrowScopeDCFields : null; + ctx.ArrowScopeDCExtraFieldsByArrow = _arrowScopeDCExtraFields.Count > 0 ? _arrowScopeDCExtraFields : null; + ApplyCapturedTopLevelVariableAccess(ctx); + ApplyCommonJsModuleAccess(ctx); + return ctx; + }, owner.Name); + value.Type.CreateType(); + } + } + + private void ApplyPrivateMethodValueContext(CompilationContext ctx, MethodBuilder method) + { + if (!_privateMethodValueOwners.TryGetValue(method, out var owner)) return; + ctx.CurrentClassName = owner.Name; + ctx.CurrentClassBuilder = owner.Owner; + ctx.IsStrictMode = true; + ctx.IsInstanceMethod = !_classes.PrivateElements.Require(owner.Name).MethodValues.Values.First(value => value.Method == method).Source.IsStatic; + ctx.GuestThisVariableName = "__this"; + ctx.HasGuestReceiver = true; + foreach (var parameter in owner.Source.TypeParams ?? []) ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; + } + + private void EmitGuestMethodValueBody(MethodBuilder method, Stmt.Function source, + Func createContext, string className) + { + var guestThis = new Stmt.Parameter(new Token(TokenType.IDENTIFIER, "__this", null, 0), "any"); + var lowered = source with { Parameters = [guestThis, .. source.Parameters] }; + if (_asyncMethodFunctionDCKeys.TryGetValue(source, out var asyncKey)) _asyncMethodFunctionDCKeys[lowered] = asyncKey; + if (_generatorMethodFunctionDCKeys.TryGetValue(source, out var generatorKey)) _generatorMethodFunctionDCKeys[lowered] = generatorKey; + if (_asyncGeneratorMethodFunctionDCKeys.TryGetValue(source, out var asyncGeneratorKey)) _asyncGeneratorMethodFunctionDCKeys[lowered] = asyncGeneratorKey; + if (source.IsAsync && source.IsGenerator) EmitAsyncGeneratorMethodBody(method, lowered, null, currentClassName: className); + else if (source.IsAsync) EmitAsyncMethodBody(method, lowered, null, currentClassName: className); + else if (source.IsGenerator) EmitGeneratorMethodBody(method, lowered, null, currentClassName: className); + else + { + var il = method.GetILGenerator(); + var ctx = createContext(); + ctx.DefineParameter("__this", 1, _types.Object); + SetupSyncMethodFunctionDisplayClass(ctx, il, source); + var parameterTypes = method.GetParameters().Skip(1).Select(parameter => parameter.ParameterType).ToArray(); + for (int i = 0; i < source.Parameters.Count; i++) ctx.DefineParameter(source.Parameters[i].Name.Lexeme, i + 2, parameterTypes[i]); + var emitter = new ILEmitter(ctx); + EmitFunctionEnvironmentPrologue(il, ctx, emitter, source.Parameters, source.Body, parameterTypes, + argumentOffset: 2, publishedArgsLeadingSkip: 1); + InitializeSyncMethodCapturedParameters(ctx, il, source, method, argumentOffset: 2); + foreach (var statement in source.Body ?? []) emitter.EmitStatement(statement); + if (emitter.HasDeferredReturns) emitter.FinalizeReturns(); + else { EmitDefaultReturnValue(il, method.ReturnType); il.Emit(OpCodes.Ret); } + } + } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs index 87551dcf5..b05737d1c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs @@ -12,7 +12,7 @@ private void DefineClassPrototypeConstructor(TypeBuilder typeBuilder) return; _classes.PrototypeConstructors[typeBuilder] = typeBuilder.DefineConstructor( - MethodAttributes.Family, + MethodAttributes.FamORAssem, CallingConventions.Standard, [_runtime.ClassPrototypes.MarkerType]); } @@ -270,11 +270,19 @@ void EmitPrototypeDataDescriptor(string propertyName, Action emitValue) EmitPrototypeDataDescriptor(methodName, () => { - il.Emit(OpCodes.Ldloc, prototypeLocal); - il.Emit(OpCodes.Ldtoken, methodBuilder); - il.Emit(OpCodes.Ldtoken, selfType); - il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); - il.Emit(OpCodes.Castclass, _types.MethodInfo); + if (DefineDeclarationReceiverMethod(typeBuilder, methodBuilder) is { } adapter) + { + il.Emit(OpCodes.Newobj, adapter.Constructor); + EmitMethodInfoLiteral(il, adapter.Method, adapter.Type); + } + else + { + il.Emit(OpCodes.Ldloc, prototypeLocal); + il.Emit(OpCodes.Ldtoken, methodBuilder); + il.Emit(OpCodes.Ldtoken, selfType); + il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + } il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); }); } @@ -354,11 +362,19 @@ void EmitStaticDataDescriptor(string propertyName, MethodBuilder methodBuilder) void EmitStaticFunction(MethodBuilder methodBuilder) { - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ldtoken, methodBuilder); - il.Emit(OpCodes.Ldtoken, selfType); - il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); - il.Emit(OpCodes.Castclass, _types.MethodInfo); + if (DefineDeclarationReceiverMethod(typeBuilder, methodBuilder) is { } adapter) + { + il.Emit(OpCodes.Newobj, adapter.Constructor); + EmitMethodInfoLiteral(il, adapter.Method, adapter.Type); + } + else + { + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldtoken, methodBuilder); + il.Emit(OpCodes.Ldtoken, selfType); + il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + } il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); } } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs b/src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs new file mode 100644 index 000000000..75dc8917c --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs @@ -0,0 +1,195 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; +using SharpTS.Parsing.Visitors; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private readonly Dictionary _runtimeClassDeclarations = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _receiverInitializers = new(ReferenceEqualityComparer.Instance); + private bool _usesRuntimeParents; + + private bool TryDefineRuntimeClassDeclaration(Stmt.Class declaration) + { + if (declaration.SuperclassExpr == null || declaration.IsDeclare) return false; + var leaf = Expr.GetSuperclassLeafName(declaration.SuperclassExpr); + if (leaf != null && ((_typeMap.Get(declaration.SuperclassExpr) is not TypeSystem.TypeInfo.Any + && (_classes.Builders.ContainsKey(GetDefinitionContext().ResolveClassName(leaf)) || _classExprs.VarToClassExpr.ContainsKey(leaf))) + || Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(leaf) + || leaf is "Array" or "Promise" or "Map" or "Set" or "WeakMap" or "WeakSet" or "Date" or "RegExp" or "Buffer")) return false; + if (declaration.SuperclassExpr is Expr.ClassExpr { Methods.Count: 0, Fields.Count: 0 }) return false; + var expression = new Expr.ClassExpr(declaration.Name, declaration.TypeParams, + declaration.SuperclassExpr, declaration.SuperclassTypeArgs, declaration.Methods, + declaration.Fields, declaration.Accessors, declaration.AutoAccessors, declaration.Interfaces, + declaration.InterfaceTypeArgs, declaration.IsAbstract, declaration.StaticInitializers); + _runtimeClassDeclarations.Add(declaration, expression); + _usesRuntimeParents = true; + if (_typeMap.GetClassType(declaration) is { } checkedType) _typeMap.SetClassExprType(expression, checkedType); + CollectClassExpression(expression); + DefineClassExpression(expression); + _classes.BlockScopedBuilders[declaration] = _classExprs.Builders[expression]; + _classes.DeclarationNames[declaration] = _classExprs.Names[expression]; + _classes.DeclarationNamespaces[declaration] = _currentNamespacePath; + return true; + } + + private bool HasRuntimeParent(Expr.ClassExpr expression) => expression.SuperclassExpr != null + && (_classExprs.Builders[expression].BaseType == null || _classExprs.Builders[expression].BaseType == _types.Object) + && expression.SuperclassExpr is not Expr.ClassExpr { Methods.Count: 0, Fields.Count: 0 }; + + private static bool SupportsReceiverInitialization(Stmt.Class declaration) + { + // The finite runtime-parent path is ordinary fields and synchronous + // methods. Other templates keep their existing CLR construction path. + if (declaration.TypeParams is { Count: > 0 } || declaration.Accessors is { Count: > 0 } + || declaration.AutoAccessors is { Count: > 0 } + || declaration.Fields.Any(field => field.IsPrivate || field.ComputedKey != null) + || declaration.Methods.Any(method => method.IsPrivate || method.IsAsync || method.IsGenerator + || method.TypeParams is { Count: > 0 } || method.ComputedKey != null + || method.Parameters.Any(parameter => parameter.IsParameterProperty || parameter.DestructuredProperties != null))) return false; + var returns = new ConstructorReplacementVisitor(); + foreach (var statement in declaration.Methods.FirstOrDefault(method => method.Name.Lexeme == "constructor")?.Body ?? []) + returns.Visit(statement); + return !returns.HasReplacement; + } + + private sealed class ConstructorReplacementVisitor : AstVisitorBase + { + public bool HasReplacement { get; private set; } + protected override void VisitReturn(Stmt.Return statement) => HasReplacement |= statement.Value != null; + protected override void VisitFunction(Stmt.Function statement) { } + protected override void VisitArrowFunction(Expr.ArrowFunction expression) { } + protected override void VisitClass(Stmt.Class statement) { } + protected override void VisitClassExpr(Expr.ClassExpr expression) { } + } + + private MethodBuilder DefineReceiverInitializer(TypeBuilder owner) + { + if (_receiverInitializers.TryGetValue(owner, out var existing)) return existing; + var method = owner.DefineMethod("$InitializeReceiver", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [_types.Object, _types.ObjectArray, _types.Object]); + _receiverInitializers.Add(owner, method); + return method; + } + + private void EmitReceiverInitializer(TypeBuilder owner, Stmt.Class declaration, Expr.ClassExpr? expression = null) + { + // These entries deliberately use an explicit guest receiver. A runtime-selected + // child need not be a CLR subtype of this template. + var method = DefineReceiverInitializer(owner); + var il = method.GetILGenerator(); + var ctx = expression == null ? CreateModuleMemberContext(il, method) + : CreateClassExpressionContext(il, expression, owner, null, method); + ctx.IsStrictMode = true; + ctx.CurrentClassBuilder = owner; + ctx.CurrentClassName = expression == null ? GetQualifiedClassDeclarationName(declaration) : _classExprs.Names[expression]; + ctx.EmittingTypeBuilder = owner; + ctx.IsInstanceMethod = true; + ctx.HasGuestReceiver = true; + ctx.GuestThisVariableName = "__this"; + ctx.ClassDefinitionParameterIndex = 2; + ctx.DefineParameter("__this", 0, _types.Object); + ApplyCapturedTopLevelVariableAccess(ctx); + var emitter = new ILEmitter(ctx); + var constructor = declaration.Methods.FirstOrDefault(source => source.Body != null && source.Name.Lexeme == "constructor" && !source.IsStatic); + if (constructor?.Body != null && ReferencesArgumentsIdentifier(constructor.Body) + && !constructor.Parameters.Any(parameter => parameter.Name.Lexeme == "arguments")) + { + var arguments = ctx.Locals.DeclareLocal("arguments", _types.ListOfObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Newobj, _runtime.Arguments.EnumerableCtor); + il.Emit(OpCodes.Stloc, arguments); + } + foreach (var (parameter, index) in (constructor?.Parameters ?? []).Select((parameter, index) => (parameter, index))) + { + var local = ctx.Locals.DeclareLocal(parameter.Name.Lexeme, _types.Object); + il.Emit(OpCodes.Ldarg_1); + if (parameter.IsRest) + { + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.ListOfObject, _types.IEnumerableOfObject)); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Call, _types.GetMethod(typeof(Math), "Min", _types.Int32, _types.Int32)); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4, index); il.Emit(OpCodes.Sub); il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Call, _types.GetMethod(typeof(Math), "Max", _types.Int32, _types.Int32)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "GetRange", _types.Int32, _types.Int32)); + } + else + { + il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.ReadArgument); + } + il.Emit(OpCodes.Stloc, local); + } + foreach (var parameter in (constructor?.Parameters ?? []).Where(parameter => parameter.DefaultValue != null)) + { + var local = ctx.Locals.GetLocal(parameter.Name.Lexeme)!; + var present = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, local); il.Emit(OpCodes.Isinst, _runtime.Sentinels.UndefinedType); il.Emit(OpCodes.Brfalse, present); + emitter.EmitExpression(parameter.DefaultValue!); emitter.EmitBoxIfNeeded(parameter.DefaultValue!); il.Emit(OpCodes.Stloc, local); + il.MarkLabel(present); + } + bool explicitSuper = constructor?.Body?.Any(ContainsSuperCall) == true; + if (!explicitSuper && declaration.SuperclassExpr != null) + { + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.GetParent); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.InitializeReceiver); + } + bool initialized = false; + if (!explicitSuper) InitializeFields(); + foreach (var statement in constructor?.Body ?? []) + { + emitter.EmitStatement(statement); + if (!initialized && ContainsSuperCall(statement)) InitializeFields(); + } + if (!initialized) InitializeFields(); + emitter.FinalizeReturns(); + il.Emit(OpCodes.Ret); + + void InitializeFields() + { + initialized = true; + foreach (var field in declaration.Fields.Where(field => !field.IsStatic && !field.IsDeclare && !field.IsPrivate)) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, field.Name.Lexeme); + if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); emitter.EmitBoxIfNeeded(field.Initializer); } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); + } + } + } + + private (TypeBuilder Type, ConstructorBuilder Constructor, MethodBuilder Method)? DefineDeclarationReceiverMethod( + TypeBuilder owner, MethodBuilder original) + { + if (!_usesRuntimeParents) return null; + var declaration = _classes.Declarations.FirstOrDefault(source => _classes.DeclarationNames.GetValueOrDefault(source) == owner.FullName); + var source = declaration?.Methods.FirstOrDefault(source => source.Body != null && source.IsStatic == original.IsStatic && source.Name.Lexeme == original.Name); + if (source == null || !SupportsReceiverInitialization(declaration!)) return null; + var type = EmitTypeDefinitions.DefineType(_moduleBuilder, $"$ReceiverMember_{owner.FullName}_{original.Name}_{(source.IsStatic ? "Static" : "Instance")}", TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, _types.ObjectDefaultCtor); il.Emit(OpCodes.Ret); + var method = type.DefineMethod("Invoke", MethodAttributes.Public, original.ReturnType, + [_types.Object, .. source.Parameters.Select(parameter => parameter.IsRest ? _types.ListOfObject : _types.Object)]); + MarkExpectsThis(method); MarkNonConstructible(method); MarkPadsUndefined(method); + MarkFunctionLength(method, source.Parameters); MarkFunctionName(method, original.Name); + EmitGuestMethodValueBody(method, source, () => + { + var ctx = CreateModuleMemberContext(method.GetILGenerator(), method); + ctx.CurrentClassBuilder = owner; ctx.CurrentClassName = owner.FullName; + ctx.EmittingTypeBuilder = type; ctx.IsStrictMode = true; ctx.IsInstanceMethod = !source.IsStatic; + ctx.HasGuestReceiver = true; ctx.GuestThisVariableName = "__this"; + ApplyCapturedTopLevelVariableAccess(ctx); + return ctx; + }, owner.FullName!); + type.CreateType(); + return (type, constructor, method); + } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs index 1a0029162..6c3c7f478 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs @@ -71,6 +71,11 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt } var emitter = new ILEmitter(ctx); + if (privateElements.StaticPresence is { } presence) + { + il.Emit(OpCodes.Newobj, typeof(HashSet).GetConstructor(Type.EmptyTypes)!); + il.Emit(OpCodes.Stsfld, presence); + } // A compact class uses weak side storage; initialize its table before // user-observable static initialization can construct or mutate an instance. @@ -169,6 +174,8 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt il.Emit(OpCodes.Stsfld, staticField); } } + if (field.IsPrivate) + EmitStaticPrivatePresence(il, privateElements, field.Name.Lexeme.TrimStart('#')); break; case Stmt.StaticBlock block: @@ -197,6 +204,7 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt emitter.EmitExpression(field.Initializer!); emitter.EmitBoxIfNeeded(field.Initializer!); il.Emit(OpCodes.Stsfld, staticPrivateField); + EmitStaticPrivatePresence(il, privateElements, fieldName); } } @@ -232,6 +240,14 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt _classes.DeferredDefinitions.MarkInitializerEmitted(deferredDefinition); } + private void EmitStaticPrivatePresence(ILGenerator il, PrivateClassElements elements, string name) + { + il.Emit(OpCodes.Ldsfld, EmitterTypeHelpers.SelfFieldReference(elements.StaticPresence!)); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Callvirt, _types.GetMethod(typeof(HashSet), "Add", typeof(string))); + il.Emit(OpCodes.Pop); + } + /// /// Emits $Runtime.RegisterSymbolAccessor(typeof(ThisClass), symbol, getter, setter, isStatic) /// for each symbol-keyed accessor recorded for this class (#266). diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.cs index c21c34860..87039bb66 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.cs @@ -20,6 +20,21 @@ namespace SharpTS.Compilation; /// public partial class ILCompiler { + private readonly struct ClassNamespaceScope : IDisposable + { + private readonly ILCompiler _compiler; + private readonly string? _previous; + + public ClassNamespaceScope(ILCompiler compiler, Stmt.Class declaration) + { + _compiler = compiler; + _previous = compiler._currentNamespacePath; + compiler._currentNamespacePath = compiler._classes.DeclarationNamespaces.GetValueOrDefault(declaration); + } + + public void Dispose() => _compiler._currentNamespacePath = _previous; + } + private static void MarkJsVariadicConstructor(ConstructorBuilder constructor) { var parameter = constructor.DefineParameter( @@ -31,13 +46,28 @@ private static void MarkJsVariadicConstructor(ConstructorBuilder constructor) private void DefineClass(Stmt.Class classStmt) { _classes.Declarations.Add(classStmt); + if (TryDefineRuntimeClassDeclaration(classStmt)) return; var ctx = GetDefinitionContext(); // Get qualified class name (includes module prefix and .NET namespace if set) string qualifiedClassName = GetQualifiedClassDeclarationName(classStmt); + var checkedClass = _typeMap.GetClassType(classStmt); + if (_currentNamespacePath is not null) + qualifiedClassName = ctx.GetQualifiedClassName( + $"$nsclass_{_currentNamespacePath.Replace('.', '_')}_{checkedClass?.Core.DeclarationId}_{classStmt.Name.Lexeme}"); + _classes.DeclarationNames[classStmt] = qualifiedClassName; + _classes.DeclarationNamespaces[classStmt] = _currentNamespacePath; + _classes.ScopedNames[(_modules.CurrentPath, _currentNamespacePath, classStmt.Name.Lexeme)] = qualifiedClassName; + if (checkedClass is not null) + { + _classes.CheckedDeclarationNames[checkedClass.Core.DeclarationId] = qualifiedClassName; + _typeMapper.RegisterCheckedClassName(checkedClass.Core.DeclarationId, qualifiedClassName); + _typeMap.SetClassType(qualifiedClassName, checkedClass); + } + // Track simple name -> module mapping for later lookups - if (_modules.CurrentPath != null && !_classes.BlockScopedNames.ContainsKey(classStmt)) + if (_modules.CurrentPath != null && _currentNamespacePath is null && !_classes.BlockScopedNames.ContainsKey(classStmt)) { _modules.ClassToModule[classStmt.Name.Lexeme] = _modules.CurrentPath; } @@ -114,6 +144,9 @@ private void DefineClass(Stmt.Class classStmt) qualifiedClassName, typeAttrs ); + typeBuilder.SetCustomAttribute( + _runtime.FunctionAttributes.FunctionNameCtor, + CustomAttributeEncoder.Encode(_runtime.FunctionAttributes.FunctionNameCtor, classStmt.Name.Lexeme)); if (_classes.BlockScopedNames.ContainsKey(classStmt)) _classes.BlockScopedBuilders[classStmt] = typeBuilder; @@ -205,7 +238,7 @@ private void DefineClass(Stmt.Class classStmt) } // Track Error subclass status (direct or transitive) - if (classStmt.SuperclassExpr != null && Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(Expr.GetSuperclassLeafName(classStmt.SuperclassExpr)!)) + if (Expr.GetSuperclassLeafName(classStmt.SuperclassExpr) is { } errorParentName && Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(errorParentName)) { _classes.ErrorSubclasses.Add(qualifiedClassName); } @@ -417,7 +450,8 @@ private void DefinePrivateClassElements( TypeBuilder typeBuilder, string className, Stmt.Class classStmt, - GenericTypeParameterBuilder[]? classGenericParams) + GenericTypeParameterBuilder[]? classGenericParams, + Expr.ClassExpr? expression = null) { // Collect private fields (IsPrivate flag indicates #field syntax) var instancePrivateFields = classStmt.Fields.Where(f => f.IsPrivate && !f.IsStatic).ToList(); @@ -506,7 +540,8 @@ private void DefinePrivateClassElements( if (methodName.StartsWith('#')) methodName = methodName[1..]; - var paramTypes = method.Parameters.Select(_ => typeof(object)).ToArray(); + var paramTypes = (expression == null ? Array.Empty() : new Type[] { _runtime.ClassDefinitions.Type }) + .Concat(method.Parameters.Select(_ => typeof(object))).ToArray(); Type returnType = ResolvePrivateMethodReturnType(method, isStatic: true); // Use Assembly (internal) visibility so nested async/generator state machines can access this method @@ -523,6 +558,10 @@ private void DefinePrivateClassElements( ? DefinePrivateInstanceBridge(typeBuilder, privateStorage, privateMethods) : null; _classes.PrivateElements.Declare(className, typeBuilder, privateStorage, privateFieldNames, privateStaticFields, privateMethods, privateStaticMethods, instanceBridge); + if (privateStaticFields.Count > 0) + _classes.PrivateElements.Require(className).StaticPresence = typeBuilder.DefineField( + "__installedStaticPrivateFields", typeof(HashSet), FieldAttributes.Assembly | FieldAttributes.Static); + DefinePrivateMethodValues(className, typeBuilder, classStmt, expression); } private PrivateInstanceBridge DefinePrivateInstanceBridge(TypeBuilder owner, FieldBuilder storage, diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 698c38c29..1e27a4f79 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -38,7 +38,8 @@ public partial class ILCompiler /// private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBase? method = null) { - var ctx = new CompilationContext(il, _typeMapper, _functions.Builders, _classes.Builders, _namespaceFields, _namespaceVarFields, _types) + var ctx = new CompilationContext(il, _typeMapper, _functions.Builders, _classes.Builders, + NamespaceFieldsForModule(_modules.CurrentPath), NamespaceVarsForModule(_modules.CurrentPath), _types) { // Closure analysis registries ClosureAnalyzer = _closures.Analyzer, @@ -81,6 +82,8 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa StaticDirectEvalStatements = _staticDirectEvalStatements, StaticIndirectEvalCalls = _staticIndirectEvalCalls, TypeMap = _typeMap, + CheckedClassNames = _classes.CheckedDeclarationNames, + ScopedClassNames = _classes.ScopedNames, DeadCode = _deadCodeInfo, TypeEmitterRegistry = _typeEmitterRegistry, BuiltInModuleEmitterRegistry = _builtInModuleEmitterRegistry, @@ -89,8 +92,12 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa ImportedNames = _importedNames, TopLevelNumericConstantValues = _topLevelNumericConstantValues, ClassExprBuilders = _classExprs.Builders, + ClassExprFactories = _classExprs.Factories, + ClassExprDefinitionFields = _classExprs.DefinitionFields, + RuntimeClassDeclarations = _runtimeClassDeclarations, ClassExprStaticMethods = _classExprs.StaticMethods, - ClassExprCaptureFields = _classExprs.CaptureFields, + ClassExprCaptureSlots = _classExprs.CaptureSlots, + ClassExprEnclosingClasses = _classExprs.EnclosingClass, DeferredClassDefinitions = _classes.DeferredDefinitions, BlockScopedClassBuilders = _classes.BlockScopedBuilders, ClassRegistry = GetClassRegistry(), @@ -166,6 +173,28 @@ private CompilationContext CreateModuleMemberContext(ILGenerator il, MethodBase? return ctx; } + private void ApplyClassDefinitionStateMachineContext(CompilationContext ctx, MethodBuilder method, bool isInstance) + { + ApplyPrivateMethodValueContext(ctx, method); + if (_classExprs.DefinitionMethods.TryGetValue(method, out var owner)) + { + ctx.CurrentClassExpr = owner.Expression; + ctx.CurrentClassBuilder = _classExprs.Builders[owner.Expression]; + ctx.ClassDefinitionOwnerField = owner.Definition; + ctx.GuestThisVariableName = "__this"; + foreach (var parameter in owner.Expression.TypeParams ?? []) + ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; + } + else + { + ctx.CurrentClassExpr = _classExprs.Builders.FirstOrDefault(entry => ReferenceEquals(entry.Value, method.DeclaringType)).Key; + } + if (ctx.CurrentClassExpr != null) + ctx.ArrowFunctionDCFields = _closures.ArrowFunctionDCFields.Count > 0 ? _closures.ArrowFunctionDCFields : null; + if (ctx.CurrentClassExpr != null && !isInstance) + ctx.ClassDefinitionVariableName = "__classDefinition"; + } + /// /// Creates the context that emits a module's (ESM or CommonJS) top-level statements. /// @@ -256,6 +285,8 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila FunctionNames = parentCtx.FunctionNames, FunctionGenericParameters = parentCtx.FunctionGenericParameters, TypeMap = parentCtx.TypeMap, + CheckedClassNames = parentCtx.CheckedClassNames, + ScopedClassNames = parentCtx.ScopedClassNames, DeadCode = parentCtx.DeadCode, AsyncMethods = null, AsyncArrowBuilders = _async.ArrowBuilders, @@ -269,13 +300,19 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila EnumToModule = parentCtx.EnumToModule, TypeEmitterRegistry = parentCtx.TypeEmitterRegistry, ClassExprBuilders = parentCtx.ClassExprBuilders, - ClassExprCaptureFields = parentCtx.ClassExprCaptureFields, + ClassExprFactories = parentCtx.ClassExprFactories, + ClassExprDefinitionFields = parentCtx.ClassExprDefinitionFields, + RuntimeClassDeclarations = parentCtx.RuntimeClassDeclarations, + ClassExprCaptureSlots = parentCtx.ClassExprCaptureSlots, + ClassExprEnclosingClasses = parentCtx.ClassExprEnclosingClasses, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, DeferredClassDefinitions = parentCtx.DeferredClassDefinitions, IsStrictMode = parentCtx.IsStrictMode, // ES2022 Private Class Elements support - inherit from parent context CurrentClassName = parentCtx.CurrentClassName, + EnclosingClassNames = parentCtx.EnclosingClassNames, CurrentClassBuilder = parentCtx.CurrentClassBuilder, + HasGuestReceiver = parentCtx.HasGuestReceiver, // Registry services ClassRegistry = parentCtx.ClassRegistry, // Entry-point display class for captured top-level variables @@ -326,6 +363,8 @@ private CompilationContext CreateDefinitionPhaseContext() return new CompilationContext(null!, _typeMapper, _functions.Builders, _classes.Builders, _namespaceFields, _namespaceVarFields, _types) { ClassToModule = _modules.ClassToModule, + CheckedClassNames = _classes.CheckedDeclarationNames, + ScopedClassNames = _classes.ScopedNames, FunctionToModule = _modules.FunctionToModule, EnumToModule = _modules.EnumToModule, IsStrictMode = _isStrictMode diff --git a/src/SharpTS/Compilation/ILCompiler.Generators.cs b/src/SharpTS/Compilation/ILCompiler.Generators.cs index b1e60e9fd..4ee670ad0 100644 --- a/src/SharpTS/Compilation/ILCompiler.Generators.cs +++ b/src/SharpTS/Compilation/ILCompiler.Generators.cs @@ -572,6 +572,8 @@ private void EmitGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function { // Analyze generator function to determine yield points and hoisted variables var analysis = _generators.Analyzer.Analyze(method); + if (isInstanceMethod && (_classExprs.DefinitionMethods.ContainsKey(methodBuilder) || _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)))) + analysis = analysis with { UsesThis = true }; // Build state machine type. A static generator method (#692) has no `this`/instance fields, so it // is set up like a free function (isInstanceMethod: false, static stub). The type name uses the @@ -581,7 +583,8 @@ private void EmitGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function $"{methodBuilder.DeclaringType!.Name}_{methodBuilder.Name}", analysis, isInstanceMethod: isInstanceMethod, - runtime: _runtime + runtime: _runtime, + hasDynamicThis: !isInstanceMethod && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)) ); RegisterStateMachine( methodBuilder, @@ -627,6 +630,7 @@ private void EmitGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function // its QUALIFIED class name so nested private member access resolves under modules — #720). ctx.CurrentClassName = currentClassName ?? methodBuilder.DeclaringType?.Name; ctx.CurrentClassBuilder = methodBuilder.DeclaringType as TypeBuilder; + ApplyClassDefinitionStateMachineContext(ctx, methodBuilder, isInstanceMethod); // Captured outer variables are read live (by reference), not snapshotted (#541). // TopLevelStaticVars covers module-level vars that aren't in the entry-point display class. ApplyCapturedTopLevelVariableAccess(ctx); diff --git a/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs b/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs index 42632d4a3..d2bbcc0d3 100644 --- a/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs +++ b/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs @@ -125,10 +125,11 @@ private void EmitIteratorMethodStub( il.Emit(OpCodes.Newobj, smBuilder.Constructor); - if (isInstanceMethod && smBuilder.ThisField != null) + if (smBuilder.ThisField != null) { il.Emit(OpCodes.Dup); // Keep state machine reference on stack - il.Emit(OpCodes.Ldarg_0); // 'this' is at arg 0 for instance methods + if (isInstanceMethod) il.Emit(OpCodes.Ldarg_0); + else il.Emit(OpCodes.Ldsfld, _runtime.FunctionValues.CurrentThisField); il.Emit(OpCodes.Stfld, smBuilder.ThisField); } diff --git a/src/SharpTS/Compilation/ILCompiler.Namespaces.cs b/src/SharpTS/Compilation/ILCompiler.Namespaces.cs index 6224c9092..0626ea64c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Namespaces.cs +++ b/src/SharpTS/Compilation/ILCompiler.Namespaces.cs @@ -14,6 +14,31 @@ public partial class ILCompiler /// See for the rationale (#567). /// private readonly Dictionary> _namespaceVarFields = []; + private readonly Dictionary<(string? Module, string Path), FieldBuilder> _namespaceFieldOwners = []; + + private string NamespaceStorageKey(string path) => GetDefinitionContext().GetQualifiedClassName(path); + + private Dictionary NamespaceFieldsForModule(string? module) + { + Dictionary fields = []; + foreach (var (owner, field) in _namespaceFieldOwners) + if (owner.Module is null) fields[owner.Path] = field; + foreach (var (owner, field) in _namespaceFieldOwners) + if (owner.Module == module) fields[owner.Path] = field; + return fields; + } + + private Dictionary> NamespaceVarsForModule(string? module) + { + Dictionary> fields = []; + foreach (var (owner, field) in _namespaceFieldOwners) + { + if (owner.Module != module) continue; + var storageKey = _namespaceFields.First(kv => ReferenceEquals(kv.Value, field)).Key; + if (_namespaceVarFields.TryGetValue(storageKey, out var variables)) fields[owner.Path] = variables; + } + return fields; + } /// /// The namespace path whose members are currently being defined or emitted, or null when @@ -57,13 +82,15 @@ private void DefineNamespaceFields(Stmt.Namespace ns, string parentPath = "") : $"{parentPath}.{ns.Name.Lexeme}"; // Create static field for this namespace if it doesn't exist - if (!_namespaceFields.ContainsKey(path)) + string storageKey = NamespaceStorageKey(path); + if (!_namespaceFields.ContainsKey(storageKey)) { var field = _programType.DefineField( - $"$ns_{path.Replace(".", "_")}", + $"$ns_{storageKey.Replace(".", "_")}", _runtime.Namespaces.Type, FieldAttributes.Public | FieldAttributes.Static); - _namespaceFields[path] = field; + _namespaceFields[storageKey] = field; + _namespaceFieldOwners[(_modules.CurrentPath, path)] = field; } // Record the enclosing namespace during the define phase too: DefineFunction routes @@ -174,6 +201,7 @@ private void ResolveNamespaceImportAliasFunctions() /// private void DefineNamespaceVarField(string nsPath, string varName) { + nsPath = NamespaceStorageKey(nsPath); if (!_namespaceVarFields.TryGetValue(nsPath, out var fields)) { fields = []; @@ -206,7 +234,7 @@ private void DefineNamespaceVarField(string nsPath, string varName) foreach (var part in nsPath.Split('.')) { prefix = prefix.Length == 0 ? part : $"{prefix}.{part}"; - if (_namespaceVarFields.TryGetValue(prefix, out var fields)) + if (_namespaceVarFields.TryGetValue(NamespaceStorageKey(prefix), out var fields)) { foreach (var (name, field) in fields) merged[name] = field; @@ -271,8 +299,9 @@ private void InitializeNamespaceFields(ILGenerator il) { // Initialize namespace fields ordered by nesting depth (parents first) // This ensures parent namespaces exist before children are added - foreach (var (nsPath, field) in _namespaceFields.OrderBy(kv => kv.Key.Count(c => c == '.'))) + foreach (var (owner, field) in _namespaceFieldOwners.OrderBy(kv => kv.Key.Path.Count(c => c == '.'))) { + string nsPath = owner.Path; // Get simple name (last part of path) string simpleName = nsPath.Contains('.') ? nsPath[(nsPath.LastIndexOf('.') + 1)..] @@ -288,7 +317,7 @@ private void InitializeNamespaceFields(ILGenerator il) if (dotIndex > 0) { string parentPath = nsPath[..dotIndex]; - if (_namespaceFields.TryGetValue(parentPath, out var parentField)) + if (_namespaceFieldOwners.TryGetValue((owner.Module, parentPath), out var parentField)) { il.Emit(OpCodes.Ldsfld, parentField); il.Emit(OpCodes.Ldstr, simpleName); diff --git a/src/SharpTS/Compilation/ILCompiler.State.cs b/src/SharpTS/Compilation/ILCompiler.State.cs index a37611c40..532214b93 100644 --- a/src/SharpTS/Compilation/ILCompiler.State.cs +++ b/src/SharpTS/Compilation/ILCompiler.State.cs @@ -23,6 +23,10 @@ private sealed class ClassCompilationState public HashSet Declarations { get; } = new(ReferenceEqualityComparer.Instance); public HashSet EmittedMethodBodies { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Builders { get; } = []; + public Dictionary DeclarationNames { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary DeclarationNamespaces { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary CheckedDeclarationNames { get; } = []; + public Dictionary<(string? Module, string? Namespace, string Name), string> ScopedNames { get; } = []; public Dictionary BlockScopedNames { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary BlockScopedBuilders { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Superclass { get; } = []; @@ -411,9 +415,12 @@ private sealed class ClassExpressionCompilationState public Dictionary> Getters { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary> Setters { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Constructors { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary DefinitionFields { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary Factories { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Superclass { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary EnclosingClass { get; } = new(ReferenceEqualityComparer.Instance); - public Dictionary> CaptureFields { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary> CaptureSlots { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary DefinitionMethods { get; } = new(ReferenceEqualityComparer.Instance); } /// diff --git a/src/SharpTS/Compilation/ILCompiler.cs b/src/SharpTS/Compilation/ILCompiler.cs index 49f0e5afd..c1cd8b831 100644 --- a/src/SharpTS/Compilation/ILCompiler.cs +++ b/src/SharpTS/Compilation/ILCompiler.cs @@ -927,6 +927,8 @@ private void Phase4_DefineDeclarations(List statements) private void PredefineTopLevelClassExpressionTypes(IEnumerable statements) { + var collector = new ClassExpressionTemplateCollector(CollectClassExpression); + foreach (var statement in statements) collector.Visit(statement); foreach (var stmt in statements) { Expr.ClassExpr? classExpr = stmt switch @@ -946,6 +948,15 @@ private void PredefineTopLevelClassExpressionTypes(IEnumerable statements) } } + private sealed class ClassExpressionTemplateCollector(Action collect) : Parsing.Visitors.AstVisitorBase + { + protected override void VisitClassExpr(Expr.ClassExpr expression) + { + collect(expression); + base.VisitClassExpr(expression); + } + } + /// /// Phase 5: Collect all arrow functions and define class expressions. /// @@ -1420,6 +1431,8 @@ private void DefineDeclarationFromStatement(Stmt stmt, bool blockScoped = false) private string GetQualifiedClassDeclarationName(Stmt.Class classStmt, bool resolve = false) { + if (_classes.DeclarationNames.TryGetValue(classStmt, out var declared)) + return declared; if (_classes.BlockScopedNames.TryGetValue(classStmt, out var generated)) return generated; var ctx = GetDefinitionContext(); diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs index e4b54475a..b850bff24 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs @@ -243,14 +243,14 @@ private void EmitDisplayInstanceFieldPopulation(Expr.ArrowFunction af, TypeBuild // Populate $functionDC field if this arrow captures function-level variables if (_ctx.ArrowFunctionDCFields?.TryGetValue(af, out var functionDCField) == true) { - if (_ctx.FunctionDisplayClassLocal != null) + if (_ctx.FunctionDisplayClassLocal?.LocalType == functionDCField.FieldType) { // In function body - use local variable IL.Emit(OpCodes.Dup); // Keep display class on stack IL.Emit(OpCodes.Ldloc, _ctx.FunctionDisplayClassLocal); IL.Emit(OpCodes.Stfld, functionDCField); } - else if (_ctx.CurrentArrowFunctionDCField != null) + else if (_ctx.CurrentArrowFunctionDCField?.FieldType == functionDCField.FieldType) { // In arrow body - get from parent arrow's $functionDC field IL.Emit(OpCodes.Dup); // Keep display class on stack @@ -258,6 +258,13 @@ private void EmitDisplayInstanceFieldPopulation(Expr.ArrowFunction af, TypeBuild IL.Emit(OpCodes.Ldfld, _ctx.CurrentArrowFunctionDCField); IL.Emit(OpCodes.Stfld, functionDCField); } + else + { + IL.Emit(OpCodes.Dup); + if (TryEmitClassDefinitionEnvironment(functionDCField.FieldType)) + IL.Emit(OpCodes.Stfld, functionDCField); + else IL.Emit(OpCodes.Pop); + } } // Populate $arrowDC field if this arrow captures arrow-scope variables. diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs index 2a4f88012..9dfd7d514 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs @@ -25,6 +25,29 @@ protected override void EmitNew(Expr.New n) var (namespaceParts, className) = ExtractQualifiedNameFromCallee(n.Callee); + if (n.Callee is Expr.Variable expressionVariable + && _ctx.VarToClassExpr?.ContainsKey(expressionVariable.Name.Lexeme) == true + && _resolver.HasVariable(expressionVariable.Name.Lexeme)) + { + EmitExpression(n.Callee); + EnsureBoxed(); + var definition = IL.DeclareLocal(_ctx.Types.Object); + IL.Emit(OpCodes.Stloc, definition); + IL.Emit(OpCodes.Ldloc, definition); + EmitArgsArrayWithSpread(n.Arguments); + IL.Emit(OpCodes.Call, _ctx.Runtime!.DynamicConstruction.Value); + SetStackUnknown(); + return; + } + + if (n.Callee is Expr.Variable constructorVariable + && _ctx.TypeMap?.Get(n.Callee) is SharpTS.TypeSystem.TypeInfo.GenericClass + && _resolver.HasVariable(constructorVariable.Name.Lexeme)) + { + EmitCalleeExprConstruction(n); + return; + } + // A block-scoped class is a runtime lexical binding to a pre-emitted // Type token. Construct through the value path so TDZ and shadowing are // respected instead of resolving the internal Type globally. @@ -50,6 +73,7 @@ protected override void EmitNew(Expr.New n) // 4. ILEmitter-specific: resolve class name with namespace imports and imported class aliases string resolvedClassName = ResolveClassNameForNew(namespaceParts, className); + resolvedClassName = _ctx.ResolveClassName(_ctx.TypeMap?.Get(n.Callee), resolvedClassName); // 5. ILEmitter-specific: external .NET type construction if (_ctx.TypeMapper.ExternalTypes.TryGetValue(className, out var externalType) || @@ -633,7 +657,13 @@ private void EmitReflectionConstructFromType(Expr.New n) { IL.Emit(OpCodes.Dup); IL.Emit(OpCodes.Ldc_I4, i); - Type genericArgument = _ctx.TypeMapper.MapTypeInfo(instantiated.TypeArguments[i]); + Type genericArgument = instantiated.TypeArguments[i] switch + { + SharpTS.TypeSystem.TypeInfo.StringLiteral => _ctx.Types.String, + SharpTS.TypeSystem.TypeInfo.NumberLiteral => _ctx.Types.Double, + SharpTS.TypeSystem.TypeInfo.BooleanLiteral => _ctx.Types.Boolean, + var argument => _ctx.TypeMapper.MapTypeInfo(argument) + }; // CLR generic arguments cannot be System.Void; erase void/never to object here. if (_ctx.Types.IsVoid(genericArgument)) genericArgument = _ctx.Types.Object; @@ -682,10 +712,14 @@ private void EmitReflectionConstructFromType(Expr.New n) IL.Emit(OpCodes.Ldc_I4_0); IL.Emit(OpCodes.Stloc, padIndexLocal); var padCheckLabel = IL.DefineLabel(); - var padNextLabel = IL.DefineLabel(); - IL.Emit(OpCodes.Br, padCheckLabel); + var padDoneLabel = IL.DefineLabel(); - IL.MarkLabel(padNextLabel); + // The loop body must be reachable in a forward scan even when the + // enclosing expression carries values on the evaluation stack (#1966). + IL.MarkLabel(padCheckLabel); + IL.Emit(OpCodes.Ldloc, padIndexLocal); + IL.Emit(OpCodes.Ldloc, arityLocal); + IL.Emit(OpCodes.Bge, padDoneLabel); IL.Emit(OpCodes.Ldloc, parametersLocal); IL.Emit(OpCodes.Ldloc, padIndexLocal); IL.Emit(OpCodes.Ldelem_Ref); @@ -705,10 +739,8 @@ private void EmitReflectionConstructFromType(Expr.New n) IL.Emit(OpCodes.Add); IL.Emit(OpCodes.Stloc, padIndexLocal); - IL.MarkLabel(padCheckLabel); - IL.Emit(OpCodes.Ldloc, padIndexLocal); - IL.Emit(OpCodes.Ldloc, arityLocal); - IL.Emit(OpCodes.Blt, padNextLabel); + IL.Emit(OpCodes.Br, padCheckLabel); + IL.MarkLabel(padDoneLabel); for (int i = 0; i < argTemps.Count; i++) { diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.cs b/src/SharpTS/Compilation/ILEmitter.Calls.cs index 0f3cd0b88..d1f753286 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.cs @@ -164,6 +164,8 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) protected override void EmitCall(Expr.Call c) { + if (TryEmitGuestSuperCall(c)) return; + if (TryEmitGuestThisCall(c)) return; if (TryEmitPromotedObjectConsumer(c)) return; if (TryEmitNumericRestCompanionCall(c)) return; // A lexical super() inside an immediately invoked arrow must still be diff --git a/src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs b/src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs new file mode 100644 index 000000000..60f62c2c7 --- /dev/null +++ b/src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs @@ -0,0 +1,48 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILEmitter +{ + // The embedded events module owns the constructor statics used by its + // helpers. A named import also has a built-in constructor binding for + // instance construction, so that binding must not redirect these statics + // to the separate internal $EventEmitter type. + private bool TryGetEventEmitterFacade(Expr receiver, string property, + out string className, out TypeBuilder builder) + { + className = string.Empty; + builder = null!; + if (property != "defaultMaxListeners" || receiver is not Expr.Variable variable) + return false; + if (_ctx.ImportedClassAliases?.TryGetValue(variable.Name.Lexeme, out var importedClassName) == true) + { + className = importedClassName; + return _ctx.Classes.TryGetValue(className, out builder!); + } + bool imported = _ctx.CurrentModulePath is { } module + && _ctx.ModuleImportFields?.TryGetValue(module, out var imports) == true + && imports.ContainsKey(variable.Name.Lexeme); + bool builtIn = _ctx.BuiltInModuleMethodBindings?.TryGetValue(variable.Name.Lexeme, out var binding) == true + && binding.ModuleName == "events" && binding.MethodName == "EventEmitter"; + if (!imported && !builtIn) return false; + className = _ctx.ResolveClassName(_ctx.TypeMap?.Get(variable), variable.Name.Lexeme); + return _ctx.Classes.TryGetValue(className, out builder!); + } + + private bool TryEmitEventEmitterFacadeGet(Expr.Get get) + { + if (!TryGetEventEmitterFacade(get.Object, get.Name.Lexeme, out var className, out var builder) + || !_ctx.ClassRegistry!.TryGetCallableStaticField(className, get.Name.Lexeme, builder, out var field)) + return false; + // Module helper writes use the constructor value's descriptor store. + // Read that same live value instead of its CLR template field. + EmitExpression(get.Object); + EmitBoxIfNeeded(get.Object); + IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); + IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectRead.Property); + SetStackUnknown(); + return true; + } +} diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index 7c60ef138..652a0068c 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -47,6 +47,8 @@ protected override void EmitVariable(Expr.Variable v) if (TryEmitDefaultParameterTdz(name)) return; + if (TryEmitClassDefinitionSelf(name)) return; + if (TryEmitClassDefinitionCapture(name)) return; if (_ctx.LexicalInitializerTdzName == name) { @@ -242,7 +244,7 @@ protected override void EmitVariable(Expr.Variable v) } // Check if it's a class - load the Type object - if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName(name), out var classType)) + if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName(_ctx.TypeMap?.Get(v), name), out var classType)) { IL.Emit(OpCodes.Ldtoken, classType); IL.Emit(OpCodes.Call, _ctx.Types.GetMethod(_ctx.Types.Type, "GetTypeFromHandle", _ctx.Types.RuntimeTypeHandle)); @@ -403,6 +405,16 @@ protected override void EmitAssign(Expr.Assign a) // rest of the module body and ultimately trips PathStackDepth at the final ret. if (TryEmitCjsAssign(a)) return; + if (TryGetClassDefinitionCaptureSlot(a.Name.Lexeme, out _)) + { + EmitExpression(a.Value); + EnsureBoxed(); + IL.Emit(OpCodes.Dup); + TryEmitStoreClassDefinitionCapture(a.Name.Lexeme); + SetStackUnknown(); + return; + } + // Promoted string-accumulator append (#857): `s = s + E` where `s` is a StringBuilder slot. // Emit `sb.Append(E)` instead of evaluating `s + E` (String.Concat) and storing — turning the // O(n²) accumulation into O(n). The analyzer promotes `s` only when every such append is in @@ -844,6 +856,19 @@ private void EmitLexicalAssignmentTdzGuard(string name) protected override void EmitThis() { + if (TryEmitGuestThis()) return; + if (_ctx.ClassDefinitionThisParameterIndex is { } parameter) + { + IL.Emit(OpCodes.Ldarg, parameter); + SetStackUnknown(); + return; + } + if (_ctx.ClassDefinitionParameterIndex != null) + { + IL.Emit(OpCodes.Ldsfld, _ctx.Runtime!.FunctionValues.CurrentThisField); + SetStackUnknown(); + return; + } _resolver.LoadThis(); SetStackUnknown(); } @@ -1129,45 +1154,7 @@ private void EmitStringRawTaggedTemplate(Expr.TaggedTemplateLiteral ttl) // hoist-aware $RegExp emission now lives in the shared base so regex literals // compile identically in plain functions, arrows, and all state-machine bodies. - protected override void EmitClassExpression(Expr.ClassExpr ce) - { - // Class expressions evaluate to the Type object at runtime. - // The type has been pre-defined during collection phase. - if (_ctx.ClassExprBuilders != null && _ctx.ClassExprBuilders.TryGetValue(ce, out var typeBuilder)) - { - EmitClassHeritageExpression(ce.SuperclassExpr, ce.Name?.Lexeme); - - if (_ctx.ClassExprCaptureFields?.TryGetValue(ce, out var captureFields) == true) - { - foreach (var (name, field) in captureFields) - { - EmitVariable(new Expr.Variable( - new Token(TokenType.IDENTIFIER, name, null, 0))); - EnsureBoxed(); - IL.Emit(OpCodes.Stsfld, field); - } - } - - // Class-expression evaluation always creates its constructor prototype; - // static elements and computed keys share the same emitted .cctor. - IL.Emit(OpCodes.Ldtoken, typeBuilder); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ClassInitialization.RunDefinition); - if (_ctx.DeferredClassDefinitions?.TryGet(ce, out var deferred) == true) - EmitDeferredComputedKeys(deferred.Registrar, deferred.Keys); - - // Load the Type object using ldtoken + GetTypeFromHandle - IL.Emit(OpCodes.Ldtoken, typeBuilder); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - SetStackUnknown(); - } - else - { - // Fallback: push null (should not happen if collection worked) - IL.Emit(OpCodes.Ldnull); - SetStackUnknown(); - } - } + protected override void EmitClassExpression(Expr.ClassExpr ce) => EmitGuestClassDefinition(ce); protected override void EmitDelete(Expr.Delete del) { diff --git a/src/SharpTS/Compilation/ILEmitter.Namespaces.cs b/src/SharpTS/Compilation/ILEmitter.Namespaces.cs index 8e6182963..184f1f4ce 100644 --- a/src/SharpTS/Compilation/ILEmitter.Namespaces.cs +++ b/src/SharpTS/Compilation/ILEmitter.Namespaces.cs @@ -141,7 +141,8 @@ private void EmitNamespaceMember(Stmt member, FieldBuilder nsField, string nsPat case Stmt.Class classStmt: // Classes are defined separately - store the Type in namespace - if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName(classStmt.Name.Lexeme), out var classType)) + if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName( + _ctx.TypeMap?.GetClassType(classStmt), classStmt.Name.Lexeme), out var classType)) { IL.Emit(OpCodes.Ldsfld, nsField); IL.Emit(OpCodes.Ldstr, classStmt.Name.Lexeme); diff --git a/src/SharpTS/Compilation/ILEmitter.Operators.cs b/src/SharpTS/Compilation/ILEmitter.Operators.cs index a7dc2f033..1cec3c4dc 100644 --- a/src/SharpTS/Compilation/ILEmitter.Operators.cs +++ b/src/SharpTS/Compilation/ILEmitter.Operators.cs @@ -746,6 +746,7 @@ private bool TryEmitExternalUnaryOperator(Expr.Unary unary) protected override void EmitCompoundAssign(Expr.CompoundAssign ca) { + if (TryGetClassDefinitionCaptureSlot(ca.Name.Lexeme, out _)) { base.EmitCompoundAssign(ca); return; } if (TryEmitExternalCompoundAssign(ca)) return; @@ -970,6 +971,7 @@ private void ConvertExternalOperatorStorage(Type storageType) protected override void EmitLogicalAssign(Expr.LogicalAssign la) { + if (TryGetClassDefinitionCaptureSlot(la.Name.Lexeme, out _)) { base.EmitLogicalAssign(la); return; } // An unresolvable reference throws before the logical operator can // inspect its value. Letting the generic store path handle this left // incompatible stack shapes at the join label (the failed store does @@ -1937,6 +1939,14 @@ private void EmitIntegerCounterIncrement(string name, bool isPlus, bool postfix) /// private void EmitStoreIncrementedVariable(string name, bool isTypedDouble, bool resultIsUnboxedDouble) { + if (TryGetClassDefinitionCaptureSlot(name, out _)) + { + if (isTypedDouble) IL.Emit(OpCodes.Box, _ctx.Types.Double); + TryEmitStoreClassDefinitionCapture(name); + if (resultIsUnboxedDouble) IL.Emit(OpCodes.Box, _ctx.Types.Double); + SetStackUnknown(); + return; + } // Per-iteration loop-binding cell (#650): write the new value through the StrongBox so closures // that captured this iteration's cell observe it. if (_ctx.CellBindingLocals.TryGetValue(name, out var cell)) diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs b/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs index 04278bac4..fc050c8d3 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs @@ -13,6 +13,10 @@ public partial class ILEmitter /// protected override void EmitGetPrivate(Expr.GetPrivate gp) { + if (TryEmitPrivateMethodValue(gp)) return; + if (TryEmitDefinitionPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestStaticPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestInstancePrivateField(gp.Object, gp.Name)) return; if (TryEmitGenericPrivateGet(gp)) return; // Get the field name without the # prefix @@ -111,6 +115,9 @@ protected override void EmitGetPrivate(Expr.GetPrivate gp) /// protected override void EmitSetPrivate(Expr.SetPrivate sp) { + if (TryEmitDefinitionPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestStaticPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestInstancePrivateField(sp.Object, sp.Name, sp.Value)) return; if (TryEmitGenericPrivateSet(sp)) return; // Get the field name without the # prefix @@ -225,6 +232,7 @@ protected override void EmitSetPrivate(Expr.SetPrivate sp) /// protected override void EmitCallPrivate(Expr.CallPrivate cp) { + if (TryEmitDefinitionPrivateCall(cp)) return; if (TryEmitGenericPrivateCall(cp)) return; // Get the method name without the # prefix diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.cs b/src/SharpTS/Compilation/ILEmitter.Properties.cs index 10063724a..ed40ea6b8 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.cs @@ -18,6 +18,8 @@ public partial class ILEmitter { protected override void EmitGet(Expr.Get g) { + if (TryEmitGuestThisGet(g)) return; + if (TryEmitEventEmitterFacadeGet(g)) return; if (TryEmitStableRecordDestructureGet(g)) return; @@ -218,7 +220,7 @@ protected override void EmitGet(Expr.Get g) // Handle static member access via 'this' in static context (static blocks, static methods) // In static blocks, 'this' refers to the class constructor, so this.property accesses static members - if (g.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.CurrentClassBuilder != null) + if (g.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.ClassDefinitionParameterIndex == null && _ctx.CurrentClassBuilder != null) { // Use cached CurrentClassName instead of linear search string? currentClassName = _ctx.CurrentClassName; @@ -237,9 +239,9 @@ protected override void EmitGet(Expr.Get g) return; // Handle static member access via class name - if (g.Object is Expr.Variable classVar) + if (g.Object is Expr.Variable classVar && !_resolver.HasVariable(classVar.Name.Lexeme)) { - string resolvedClassName = _ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = _ctx.ResolveClassName(_ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (_ctx.Classes.TryGetValue(resolvedClassName, out var classBuilder)) { // Try static getter first (for auto-accessors and explicit static accessors) @@ -315,18 +317,7 @@ protected override void EmitGet(Expr.Get g) } } - // Handle static member access via class expression variable - if (g.Object is Expr.Variable classExprVar && - _ctx.VarToClassExpr != null && - _ctx.VarToClassExpr.TryGetValue(classExprVar.Name.Lexeme, out var classExpr) && - _ctx.ClassExprStaticFields != null && - _ctx.ClassExprStaticFields.TryGetValue(classExpr, out var exprStaticFields) && - exprStaticFields.TryGetValue(g.Name.Lexeme, out var exprStaticField)) - { - IL.Emit(OpCodes.Ldsfld, exprStaticField); - SetStackUnknown(); - return; - } + // Handle static property access on external .NET types (@DotNetType) if (g.Object is Expr.Variable extVar && _ctx.TypeMapper.ExternalTypes.TryGetValue(extVar.Name.Lexeme, out var externalType)) @@ -969,6 +960,7 @@ private void SetStackTypeForFieldType(Type fieldType) protected override void EmitSet(Expr.Set s) { + if (TryEmitGuestThisSet(s)) return; // CommonJS: `module.exports = X` writes → stsfld $exports if (TryEmitCjsSet(s)) return; @@ -1053,7 +1045,7 @@ protected override void EmitSet(Expr.Set s) } // Handle static property assignment via 'this' in static context (static blocks, static methods) - if (s.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.CurrentClassBuilder != null) + if (s.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.ClassDefinitionParameterIndex == null && _ctx.CurrentClassBuilder != null) { // First check for class expressions if (_ctx.CurrentClassExpr != null && @@ -1084,9 +1076,9 @@ protected override void EmitSet(Expr.Set s) // Handle static property assignment via class name: delegate to EmitStaticMemberSet // which handles setters (auto-accessor + explicit), regular static fields, and private // static fields with correct signature-driven coercion + return-value handling. - if (s.Object is Expr.Variable classVar) + if (s.Object is Expr.Variable classVar && !_resolver.HasVariable(classVar.Name.Lexeme)) { - string resolvedClassName = _ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = _ctx.ResolveClassName(_ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (_ctx.Classes.TryGetValue(resolvedClassName, out var classBuilder)) { if (EmitStaticMemberSet(resolvedClassName, classBuilder, s.Name.Lexeme, s.Value)) @@ -2500,7 +2492,7 @@ private bool TryEmitDirectGetterCall(Expr receiver, TypeInfo? receiverType, stri return true; } - string className = _ctx.ResolveClassName(simpleClassName); + string className = _ctx.ResolveClassName(receiverType, simpleClassName); // Convert TypeScript camelCase property name to .NET PascalCase for lookup string pascalPropertyName = NamingConventions.ToPascalCase(propertyName); @@ -2587,7 +2579,7 @@ private bool TryEmitDirectSetterCall(Expr receiver, TypeInfo? receiverType, stri return true; } - string className = _ctx.ResolveClassName(simpleClassName); + string className = _ctx.ResolveClassName(receiverType, simpleClassName); // Convert TypeScript camelCase property name to .NET PascalCase for lookup string pascalPropertyName = NamingConventions.ToPascalCase(propertyName); diff --git a/src/SharpTS/Compilation/ILEmitter.Statements.cs b/src/SharpTS/Compilation/ILEmitter.Statements.cs index ccdc1fb47..3119ec07f 100644 --- a/src/SharpTS/Compilation/ILEmitter.Statements.cs +++ b/src/SharpTS/Compilation/ILEmitter.Statements.cs @@ -3279,6 +3279,18 @@ static void CollectDirectLexicalNames(Stmt statement, HashSet names) private void EmitBlockScopedClassDeclaration(Stmt.Class classStmt) { + if (_ctx.RuntimeClassDeclarations?.TryGetValue(classStmt, out var runtimeExpression) == true) + { + EmitGuestClassDefinition(runtimeExpression); + var definition = IL.DeclareLocal(_ctx.Types.Object); + IL.Emit(OpCodes.Stloc, definition); + if (_ctx.Locals.GetLocal(classStmt.Name.Lexeme) is { } binding) + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Stloc, binding); + } + return; + } var scopedClasses = _ctx.BlockScopedClassBuilders; TypeBuilder? builder = null; bool isBlockScoped = scopedClasses != null diff --git a/src/SharpTS/Compilation/NestedFunctionLifter.cs b/src/SharpTS/Compilation/NestedFunctionLifter.cs index 6fa5c8582..9582164f2 100644 --- a/src/SharpTS/Compilation/NestedFunctionLifter.cs +++ b/src/SharpTS/Compilation/NestedFunctionLifter.cs @@ -922,6 +922,11 @@ private Expr ProcessExpr(Expr expr) var obj = ProcessExpr(e.Object); var value = ProcessExpr(e.Value); return ReferenceEquals(obj, e.Object) && ReferenceEquals(value, e.Value) ? e : e with { Object = obj, Value = value }; } + case Expr.PrivateIn e: + { + var obj = ProcessExpr(e.Object); + return ReferenceEquals(obj, e.Object) ? e : e with { Object = obj }; + } case Expr.GetPrivate e: { var obj = ProcessExpr(e.Object); diff --git a/src/SharpTS/Compilation/ParameterTypeResolver.cs b/src/SharpTS/Compilation/ParameterTypeResolver.cs index 71eeb9468..8cf5e3a08 100644 --- a/src/SharpTS/Compilation/ParameterTypeResolver.cs +++ b/src/SharpTS/Compilation/ParameterTypeResolver.cs @@ -185,6 +185,8 @@ private static Type WidenIfUndefinedReachableParam(Type mapped, Stmt.Parameter p /// private static Type CoerceParamSlotType(Type mapped, TSTypeInfo source, TypeMapper typeMapper) { + if (mapped == typeof(Delegate) || mapped.IsSubclassOf(typeof(Delegate))) + return typeof(object); if (typeMapper.IsDynamicRuntimeType(mapped)) return typeof(object); if (Nullable.GetUnderlyingType(mapped) != null) diff --git a/src/SharpTS/Compilation/PrivateClassElementRegistry.cs b/src/SharpTS/Compilation/PrivateClassElementRegistry.cs index 05bcad475..5d0b0192e 100644 --- a/src/SharpTS/Compilation/PrivateClassElementRegistry.cs +++ b/src/SharpTS/Compilation/PrivateClassElementRegistry.cs @@ -53,11 +53,34 @@ internal void MarkBodiesEmitted(string name) EnsureMutable(); var declaration = Require(name); if (declaration.Methods.Values.Concat(declaration.StaticMethods.Values).Any(method => method.GetILGenerator().ILOffset == 0) + || declaration.MethodValues.Values.Any(value => value.Method.GetILGenerator().ILOffset == 0 + || value.Constructor.GetILGenerator().ILOffset == 0 || value.Initializer?.GetILGenerator().ILOffset == 0) || (declaration.InstanceBridge is { } bridge && bridge.StorageImplementation.GetILGenerator().ILOffset == 0) || !_emitted.Add(_names[name])) throw new InvalidOperationException("Private class method bodies are empty or already emitted."); } + internal void DeclareMethodValue(string owner, string name, PrivateMethodValue value) + { + EnsureMutable(); + var declaration = Require(owner); + var methods = value.Source.IsStatic ? declaration.StaticMethods : declaration.Methods; + if (_emitted.Contains(_names[owner]) || !value.Source.IsPrivate || !methods.ContainsKey(name) + || value.Type.Module != _names[owner].Module || value.Type.IsGenericType || value.Method.IsGenericMethod + || value.Method.DeclaringType != value.Type + || value.Method.IsStatic || value.Constructor.DeclaringType != value.Type + || value.Method.GetParameters().FirstOrDefault()?.ParameterType != typeof(object) + || value.Method.GetParameters().Length != value.Source.Parameters.Count + 1 + || value.Constructor.GetParameters().Length != (value.Definition == null ? 0 : 1) + || (value.Definition != null && value.Constructor.GetParameters()[0].ParameterType != value.Definition.FieldType) + || (value.Cache != null) != (value.Initializer != null) + || (value.Cache != null && (value.Cache.DeclaringType != value.Type || !value.Cache.IsStatic)) + || (value.Initializer != null && (value.Initializer.DeclaringType != value.Type || !value.Initializer.IsStatic)) + || (value.Cache == null && (value.Definition?.DeclaringType != value.Type || value.Definition.IsStatic)) + || !declaration.MutableMethodValues.TryAdd(name, value)) + throw new InvalidOperationException("Invalid or duplicate private method value declaration."); + } + internal void CompleteEmission() { EnsureMutable(); @@ -76,8 +99,11 @@ private void EnsureMutable() /// Immutable declaration views; field order and builder identity are preserved. public sealed class PrivateClassElements { + internal Dictionary MutableMethodValues { get; } = new(StringComparer.Ordinal); + public IReadOnlyDictionary MethodValues { get; } public PrivateInstanceBridge? InstanceBridge { get; } public FieldBuilder? Storage { get; } + public FieldBuilder? StaticPresence { get; internal set; } public IReadOnlyList FieldNames { get; } public IReadOnlyDictionary StaticFields { get; } public IReadOnlyDictionary Methods { get; } @@ -87,6 +113,7 @@ internal PrivateClassElements(FieldBuilder? storage, IEnumerable fieldNa IReadOnlyDictionary staticFields, IReadOnlyDictionary methods, IReadOnlyDictionary staticMethods, PrivateInstanceBridge? instanceBridge) { + MethodValues = new ReadOnlyDictionary(MutableMethodValues); InstanceBridge = instanceBridge; Storage = storage; FieldNames = Array.AsReadOnly(fieldNames.ToArray()); @@ -96,6 +123,10 @@ internal PrivateClassElements(FieldBuilder? storage, IEnumerable fieldNa } } +public sealed record PrivateMethodValue(TypeBuilder Type, ConstructorBuilder Constructor, + MethodBuilder Method, FieldBuilder? Cache, ConstructorBuilder? Initializer, SharpTS.Parsing.Stmt.Function Source, + FieldBuilder? Definition = null); + /// Non-generic dispatch surface for a generic class's private instance elements. public sealed class PrivateInstanceBridge { diff --git a/src/SharpTS/Compilation/Registries/ClassRegistry.cs b/src/SharpTS/Compilation/Registries/ClassRegistry.cs index 3acbdb2db..33870e62a 100644 --- a/src/SharpTS/Compilation/Registries/ClassRegistry.cs +++ b/src/SharpTS/Compilation/Registries/ClassRegistry.cs @@ -14,6 +14,9 @@ namespace SharpTS.Compilation.Registries; /// public sealed class ClassRegistry { + public PrivateMethodValue? GetPrivateMethodValue(string owner, string name) => + _privateElements.TryGet(owner, out var declaration) && declaration.MethodValues.TryGetValue(name, out var value) ? value : null; + public PrivateClassElements? GetPrivateElements(string owner) => _privateElements.TryGet(owner, out var declaration) ? declaration : null; // Core class builders private readonly Dictionary _builders; private readonly Dictionary _superclass; diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs new file mode 100644 index 000000000..bbfcf9cd1 --- /dev/null +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs @@ -0,0 +1,95 @@ +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + private void EmitClassCaptureBodies(TypeBuilder captureType, FieldBuilder owner, FieldBuilder field, + FieldBuilder captures, MethodBuilder read, MethodBuilder write, MethodBuilder find) + { + var il = read.GetILGenerator(); + var cell = il.DeclareLocal(captureType); + var snapshot = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, captureType); + il.Emit(OpCodes.Stloc, cell); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Brfalse, snapshot); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Callvirt, _types.FieldInfoGetValue); + il.Emit(OpCodes.Ret); + il.MarkLabel(snapshot); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + + il = write.GetILGenerator(); + cell = il.DeclareLocal(captureType); + snapshot = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Isinst, captureType); + il.Emit(OpCodes.Stloc, cell); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Brfalse, snapshot); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.FieldInfo, "SetValue", _types.Object, _types.Object)); + il.Emit(OpCodes.Ret); + il.MarkLabel(snapshot); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Ret); + + il = find.GetILGenerator(); + var index = il.DeclareLocal(_types.Int32); + cell = il.DeclareLocal(captureType); + var loop = il.DefineLabel(); + var next = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, index); + il.MarkLabel(loop); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, done); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Isinst, captureType); + il.Emit(OpCodes.Stloc, cell); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsInstanceOfType", _types.Object)); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Ret); + il.MarkLabel(next); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, index); + il.Emit(OpCodes.Br, loop); + il.MarkLabel(done); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); + } +} diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs new file mode 100644 index 000000000..357deb644 --- /dev/null +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs @@ -0,0 +1,103 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + private void EmitRuntimeParentBodies(EmittedRuntime runtime) + { + var definitions = runtime.ClassDefinitions; + var il = definitions.ValidateParent.GetILGenerator(); + var supported = il.DefineLabel(); + var parentType = il.DeclareLocal(_types.Type); + var plainType = il.DefineLabel(); var inspectType = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, definitions.Type); il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, plainType); il.Emit(OpCodes.Ldfld, definitions.Template); il.Emit(OpCodes.Br, inspectType); + il.MarkLabel(plainType); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, _types.Type); + il.MarkLabel(inspectType); il.Emit(OpCodes.Stloc, parentType); + var unsupported = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, parentType); il.Emit(OpCodes.Brfalse, unsupported); + il.Emit(OpCodes.Ldloc, parentType); il.Emit(OpCodes.Ldstr, "$InitializeReceiver"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetMethod", _types.String)); + il.Emit(OpCodes.Brtrue, supported); + il.MarkLabel(unsupported); + GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, + "Runtime superclass must be an ordinary user class constructor"); + il.MarkLabel(supported); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ret); + + il = definitions.GetParent.GetILGenerator(); + var staticOwner = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, definitions.Type); il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, staticOwner); il.Emit(OpCodes.Ldfld, definitions.Parent); il.Emit(OpCodes.Ret); + il.MarkLabel(staticOwner); il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, _types.Type); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "BaseType")!.GetGetMethod()!); il.Emit(OpCodes.Ret); + + il = definitions.ReadArgument.GetILGenerator(); + var missing = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, missing); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldelem_Ref); il.Emit(OpCodes.Ret); + il.MarkLabel(missing); il.Emit(OpCodes.Ldsfld, runtime.Sentinels.UndefinedInstance); il.Emit(OpCodes.Ret); + + il = definitions.InitializeReceiver.GetILGenerator(); + var nothing = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brfalse, nothing); + var template = il.DeclareLocal(_types.Type); + var useType = il.DefineLabel(); var haveType = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, definitions.Type); il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, useType); il.Emit(OpCodes.Ldfld, definitions.Template); il.Emit(OpCodes.Br, haveType); + il.MarkLabel(useType); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, _types.Type); + il.MarkLabel(haveType); il.Emit(OpCodes.Stloc, template); + var adapter = il.DeclareLocal(_types.MethodInfo); + il.Emit(OpCodes.Ldloc, template); il.Emit(OpCodes.Ldstr, "$InitializeReceiver"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetMethod", _types.String)); il.Emit(OpCodes.Stloc, adapter); + var haveAdapter = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, adapter); il.Emit(OpCodes.Brtrue, haveAdapter); + il.Emit(OpCodes.Ldloc, template); il.Emit(OpCodes.Ldtoken, _types.Object); il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Beq, nothing); + GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, + "Runtime superclass initialization requires an ordinary user class constructor"); + il.MarkLabel(haveAdapter); + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldloc, adapter); il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldc_I4_3); il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_2); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "Invoke", _types.Object, _types.ObjectArray)); il.Emit(OpCodes.Pop); + il.BeginCatchBlock(typeof(TargetInvocationException)); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Exception, "InnerException")!.GetGetMethod()!); + il.Emit(OpCodes.Throw); + il.EndExceptionBlock(); il.MarkLabel(nothing); il.Emit(OpCodes.Ret); + + il = definitions.ReadPrototypeProperty.GetILGenerator(); + var prototype = il.DeclareLocal(_types.Object); var descriptor = il.DeclareLocal(runtime.DescriptorStorage.DescriptorType); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stloc, prototype); + var loop = il.DefineLabel(); var next = il.DefineLabel(); var absent = il.DefineLabel(); var data = il.DefineLabel(); + il.MarkLabel(loop); il.Emit(OpCodes.Ldloc, prototype); il.Emit(OpCodes.Brfalse, absent); + il.Emit(OpCodes.Ldloc, prototype); il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.DescriptorStorage.GetPropertyDescriptor); il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, descriptor); il.Emit(OpCodes.Callvirt, runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, data); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Newarr, _types.Object); il.Emit(OpCodes.Call, runtime.Invocation.Method); il.Emit(OpCodes.Ret); + il.MarkLabel(data); il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, runtime.DescriptorStorage.DescriptorValue.GetGetMethod()!); il.Emit(OpCodes.Ret); + il.MarkLabel(next); il.Emit(OpCodes.Ldloc, prototype); il.Emit(OpCodes.Call, runtime.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); il.Emit(OpCodes.Br, loop); + il.MarkLabel(absent); il.Emit(OpCodes.Ldsfld, runtime.Sentinels.UndefinedInstance); il.Emit(OpCodes.Ret); + + il = definitions.ReadSuper.GetILGenerator(); + var instanceSuper = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_3); il.Emit(OpCodes.Brfalse, instanceSuper); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, definitions.GetParent); il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); il.Emit(OpCodes.Ret); + il.MarkLabel(instanceSuper); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, definitions.GetParent); + il.Emit(OpCodes.Ldstr, "prototype"); il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Call, definitions.ReadPrototypeProperty); il.Emit(OpCodes.Ret); + } +} diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs new file mode 100644 index 000000000..76973f494 --- /dev/null +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs @@ -0,0 +1,329 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefinitionRuntime definitions) + { + var contract = module.DefineType("$IClassDefinitionInstance", + TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract); + var getter = contract.DefineMethod("$GetClassDefinition", + MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, + _types.Object, Type.EmptyTypes); + var instanceInterface = contract.CreateType()!; + var type = module.DefineType("$ClassDefinition", TypeAttributes.Public | TypeAttributes.Sealed); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ret); + var factoryType = typeof(Func); + var template = type.DefineField("Template", _types.Type, FieldAttributes.Assembly); + var factory = type.DefineField("Factory", factoryType, FieldAttributes.Assembly); + var prototype = type.DefineField("Prototype", _types.Object, FieldAttributes.Assembly); + var keys = type.DefineField("Keys", _types.ObjectArray, FieldAttributes.Assembly); + var parent = type.DefineField("Parent", _types.Object, FieldAttributes.Assembly); + var captures = type.DefineField("Captures", _types.ObjectArray, FieldAttributes.Assembly); + var privateMembers = type.DefineField("PrivateMembers", _types.DictionaryStringObject, FieldAttributes.Assembly); + var create = _features.UsesUserClasses + ? type.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.Static, + type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object, _types.ObjectArray, _types.ObjectArray]) : null; + var construct = type.DefineMethod("Construct", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.ObjectArray]); + var read = type.DefineMethod("ReadProperty", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.String]); + var invoke = type.DefineMethod("Invoke", MethodAttributes.Public, _types.Object, [_types.ObjectArray]); + TypeBuilder? captureType = null; + FieldBuilder? captureOwner = null, captureField = null; + ConstructorBuilder? captureConstructor = null; + MethodBuilder? readCapture = null, writeCapture = null, findEnvironment = null; + if (_features.UsesUserClasses) + { + captureType = module.DefineType("$ClassCapture", TypeAttributes.Public | TypeAttributes.Sealed); + captureOwner = captureType.DefineField("Owner", _types.Object, FieldAttributes.Assembly); + captureField = captureType.DefineField("Field", _types.FieldInfo, FieldAttributes.Assembly); + captureConstructor = captureType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + [_types.Object, _types.FieldInfo]); + il = captureConstructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, captureOwner); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stfld, captureField); + il.Emit(OpCodes.Ret); + readCapture = type.DefineMethod("ReadCapture", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]); + writeCapture = type.DefineMethod("WriteCapture", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [type, _types.Int32, _types.Object]); + findEnvironment = type.DefineMethod("FindEnvironment", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.Type]); + EmitClassCaptureBodies(captureType, captureOwner, captureField, captures, readCapture, writeCapture, findEnvironment); + captureType.CreateType(); + } + var validateParent = _features.UsesUserClasses + ? type.DefineMethod("ValidateParent", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]) : null; + var initializeReceiver = _features.UsesUserClasses + ? type.DefineMethod("InitializeReceiver", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [_types.Object, _types.Object, _types.ObjectArray]) : null; + var getParent = _features.UsesUserClasses + ? type.DefineMethod("GetParent", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]) : null; + var readArgument = _features.UsesUserClasses + ? type.DefineMethod("ReadArgument", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.ObjectArray, _types.Int32]) : null; + var readPrototypeProperty = _features.UsesUserClasses + ? type.DefineMethod("ReadPrototypeProperty", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object, _types.Object, _types.String]) : null; + var readSuper = _features.UsesUserClasses + ? type.DefineMethod("ReadSuper", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object, _types.Object, _types.String, _types.Boolean]) : null; + definitions.Declare(new(type, instanceInterface, instanceInterface.GetMethod(getter.Name)!, + constructor, template, factory, prototype, keys, parent, captures, privateMembers, create, construct, read, invoke, + captureType, captureConstructor, captureOwner, captureField, readCapture, writeCapture, findEnvironment, + validateParent, initializeReceiver, getParent, readArgument, readPrototypeProperty, readSuper)); + } + + private void EmitClassDefinitionCreation(EmittedRuntime runtime) + { + var definitions = runtime.ClassDefinitions; + var storage = runtime.DescriptorStorage; + var il = definitions.Create.GetILGenerator(); + var definition = il.DeclareLocal(definitions.Type); + var prototype = il.DeclareLocal(_types.Object); + var templatePrototype = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Newobj, definitions.Constructor); + il.Emit(OpCodes.Stloc, definition); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stfld, definitions.Template); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, definitions.Factory); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)5); + il.Emit(OpCodes.Stfld, definitions.Keys); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)4); + il.Emit(OpCodes.Stfld, definitions.Parent); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)6); + il.Emit(OpCodes.Stfld, definitions.Captures); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + il.Emit(OpCodes.Stfld, definitions.PrivateMembers); + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + il.Emit(OpCodes.Newobj, runtime.ObjectStorage.Constructor); + il.Emit(OpCodes.Stloc, prototype); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Stfld, definitions.Prototype); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ClassPrototypes.Get); + il.Emit(OpCodes.Stloc, templatePrototype); + + // Copy descriptors into a fresh prototype; descriptor flags must not be + // shared with another evaluation of the same syntax node. + var keys = il.DeclareLocal(_types.ListOfObject); + il.Emit(OpCodes.Ldloc, templatePrototype); + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + il.Emit(OpCodes.Call, storage.GetAllExtraKeys); + il.Emit(OpCodes.Stloc, keys); + var index = il.DeclareLocal(_types.Int32); + var key = il.DeclareLocal(_types.String); + var original = il.DeclareLocal(storage.DescriptorType); + var copy = il.DeclareLocal(storage.DescriptorType); + var loop = il.DefineLabel(); + var next = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, index); + il.MarkLabel(loop); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldloc, keys); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count")!.GetGetMethod()!); + il.Emit(OpCodes.Bge, done); + il.Emit(OpCodes.Ldloc, keys); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "get_Item", _types.Int32)); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Stloc, key); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, templatePrototype); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, original); + il.Emit(OpCodes.Newobj, storage.DescriptorConstructor); + il.Emit(OpCodes.Stloc, copy); + foreach (var property in new[] { storage.DescriptorValue, storage.DescriptorGetter, + storage.DescriptorSetter, storage.DescriptorWritable, storage.DescriptorEnumerable, + storage.DescriptorConfigurable }) + { + il.Emit(OpCodes.Ldloc, copy); + il.Emit(OpCodes.Ldloc, original); + il.Emit(OpCodes.Callvirt, property.GetGetMethod()!); + il.Emit(OpCodes.Callvirt, property.GetSetMethod()!); + } + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Ldloc, copy); + il.Emit(OpCodes.Call, storage.DefineProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(next); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, index); + il.Emit(OpCodes.Br, loop); + il.MarkLabel(done); + + var basePrototype = il.DeclareLocal(_types.Object); + var noParent = il.DefineLabel(); + var haveParent = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_S, (byte)4); + il.Emit(OpCodes.Brfalse, noParent); + il.Emit(OpCodes.Ldarg_S, (byte)4); + il.Emit(OpCodes.Ldstr, "prototype"); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Stloc, basePrototype); + il.Emit(OpCodes.Ldloc, basePrototype); + il.Emit(OpCodes.Brfalse, noParent); + il.Emit(OpCodes.Ldloc, basePrototype); + il.Emit(OpCodes.Isinst, runtime.Sentinels.UndefinedType); + il.Emit(OpCodes.Brtrue, noParent); + il.Emit(OpCodes.Br, haveParent); + il.MarkLabel(noParent); + il.Emit(OpCodes.Ldloc, templatePrototype); + il.Emit(OpCodes.Call, runtime.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, basePrototype); + il.MarkLabel(haveParent); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Ldloc, basePrototype); + il.Emit(OpCodes.Call, storage.SetPrototype); + EmitDataDescriptor(prototype, "constructor", () => il.Emit(OpCodes.Ldloc, definition), true); + EmitDataDescriptor(definition, "prototype", () => il.Emit(OpCodes.Ldloc, prototype), false, false); + EmitDataDescriptor(definition, "name", () => il.Emit(OpCodes.Ldarg_2), false); + EmitDataDescriptor(definition, "length", () => + { + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Box, _types.Double); + }, false); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)4); + var parentPresent = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, parentPresent); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldsfld, runtime.FunctionPrototypes.Prototype); + il.MarkLabel(parentPresent); + il.Emit(OpCodes.Call, storage.SetPrototype); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ret); + + void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writable, bool configurable = true) + { + var descriptor = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Newobj, storage.DescriptorConstructor); + il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); + value(); + il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetSetMethod()!); + foreach (var (property, flag) in new[] { (storage.DescriptorWritable, writable), + (storage.DescriptorEnumerable, false), (storage.DescriptorConfigurable, configurable) }) + { + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(flag ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, property.GetSetMethod()!); + } + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Call, storage.DefineProperty); + il.Emit(OpCodes.Pop); + } + } + + private void EmitClassDefinitionBodies(EmittedRuntime runtime) + { + var definitions = runtime.ClassDefinitions; + var storage = runtime.DescriptorStorage; + if (definitions.HasRuntimeParentHelpers) + EmitClassDefinitionCreation(runtime); + var il = definitions.Construct.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Factory); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(typeof(Func), "Invoke", _types.ObjectArray, _types.Object)); + il.Emit(OpCodes.Ret); + + il = definitions.ReadProperty.GetILGenerator(); + var ownDescriptor = il.DeclareLocal(storage.DescriptorType); + var inheritedProperty = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, ownDescriptor); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Brfalse, inheritedProperty); + var dataProperty = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, dataProperty); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Call, runtime.Invocation.Method); + il.Emit(OpCodes.Ret); + il.MarkLabel(dataProperty); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetGetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(inheritedProperty); + var noParentProperty = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Parent); + il.Emit(OpCodes.Brfalse, noParentProperty); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Parent); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Ret); + il.MarkLabel(noParentProperty); + var ordinary = il.DefineLabel(); + foreach (var name in new[] { "bind", "call", "apply", "name", "length", "prototype" }) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brtrue, ordinary); + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Template); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Ret); + il.MarkLabel(ordinary); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FunctionIntrospection.GetProperty); + il.Emit(OpCodes.Ret); + + il = definitions.Invoke.GetILGenerator(); + GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, + "Class constructor cannot be invoked without 'new'"); + if (definitions.HasRuntimeParentHelpers) + EmitRuntimeParentBodies(runtime); + definitions.Type.CreateType(); + definitions.CompleteEmission(); + } + +} diff --git a/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs b/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs index 1529b36ac..b4d7ad990 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct ConstructDynamicValueInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedBoxedPrimitiveRuntime BoxedPrimitives, EmittedErrorRuntime Errors, EmittedFunctionConstructionRuntime FunctionConstruction, @@ -43,6 +44,17 @@ ConstructDynamicValueInputs inputs var il = method.GetILGenerator(); var throwLabel = il.DefineLabel(); + var notClassDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notClassDefinition); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ClassDefinitions.Construct); + il.Emit(OpCodes.Ret); + il.MarkLabel(notClassDefinition); + il.Emit(OpCodes.Pop); + // null / $Undefined → TypeError il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brfalse, throwLabel); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs b/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs index 784001e36..3a23cc24e 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { internal readonly record struct NewOnFunctionInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedDescriptorStorageRuntime DescriptorStorage, EmittedErrorRuntime Errors, EmittedFunctionBindingRuntime FunctionBindings, @@ -45,6 +46,17 @@ NewOnFunctionInputs inputs var il = method.GetILGenerator(); + var notClassDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notClassDefinition); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ClassDefinitions.Construct); + il.Emit(OpCodes.Ret); + il.MarkLabel(notClassDefinition); + il.Emit(OpCodes.Pop); + var newObjLocal = il.DeclareLocal(inputs.ObjectStorage.Type); var resultLocal = il.DeclareLocal(_types.Object); var prevThisLocal = il.DeclareLocal(_types.Object); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs b/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs index b759a809b..e0978fc79 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs @@ -33,6 +33,7 @@ EmittedInvocationRuntime Invocation ); private readonly record struct FunctionBindWrapperClassInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedFunctionValueRuntime FunctionValues, EmittedArrayOperationsRuntime ArrayOperations, EmittedMapRuntime? Map, @@ -1494,6 +1495,7 @@ void AcceptCallable(Type callableType) il.Emit(OpCodes.Brtrue, callableTargetLabel); } AcceptCallable(inputs.FunctionValues.Type); + AcceptCallable(inputs.ClassDefinitions.Type); AcceptCallable(functionBindings.BoundType); AcceptCallable(functionBindings.AnyType); AcceptCallable(functionBindings.BindType); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs b/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs index a395db1bc..6a41864de 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs @@ -32,6 +32,7 @@ MethodInfo GetIndex private readonly record struct IteratorFunctionInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedDescriptorStorageRuntime DescriptorStorage, EmittedFunctionConstructionRuntime FunctionConstruction, EmittedGeneratorRuntime Generators, @@ -879,7 +880,7 @@ private void EmitIteratorMethodsBasic(TypeBuilder typeBuilder, EmittedRuntime ru EmitGetIteratorFunction( typeBuilder, runtime.IteratorProtocol, - new IteratorFunctionInputs(runtime.DescriptorStorage, runtime.FunctionConstruction, runtime.Generators, runtime.NodeStreams, runtime.ObjectRead, runtime.SymbolAccessors, runtime.Symbols, runtime.Sentinels.UndefinedInstance) + new IteratorFunctionInputs(runtime.ClassDefinitions, runtime.DescriptorStorage, runtime.FunctionConstruction, runtime.Generators, runtime.NodeStreams, runtime.ObjectRead, runtime.SymbolAccessors, runtime.Symbols, runtime.Sentinels.UndefinedInstance) ); EmitIteratorClose( typeBuilder, @@ -1267,6 +1268,15 @@ private void EmitGetIteratorFunction(TypeBuilder typeBuilder, EmittedIteratorPro il.Emit(OpCodes.Ret); il.MarkLabel(tryRegistryLabel); + var ordinaryRegistry = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Brfalse, ordinaryRegistry); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ObjectRead.Index); + il.Emit(OpCodes.Ret); + il.MarkLabel(ordinaryRegistry); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, inputs.SymbolAccessors.FindMethod); @@ -1274,7 +1284,6 @@ private void EmitGetIteratorFunction(TypeBuilder typeBuilder, EmittedIteratorPro il.Emit(OpCodes.Brtrue, registryValueLabel); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Br, returnUndefinedLabel); - il.MarkLabel(registryValueLabel); il.Emit(OpCodes.Ret); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs index 0b46e76af..24602ef4b 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct SetIndexInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedArrayStorageRuntime ArrayStorage, EmittedBufferRuntime? Buffer, @@ -33,6 +34,7 @@ Type UndefinedType ); private readonly record struct GetIndexInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedArrayStorageRuntime ArrayStorage, TypeBuilder BoundTSFunctionType, @@ -46,6 +48,7 @@ private readonly record struct GetIndexInputs( MethodBuilder InvokeMethodUnwrapped, MethodBuilder InvokeMethodValue, EmittedObjectDescriptorRuntime ObjectDescriptors, + EmittedObjectPrototypeRuntime ObjectPrototypes, EmittedObjectStorageRuntime ObjectStorage, EmittedRegExpRuntime RegExps, EmittedStringCoercionRuntime StringCoercion, @@ -133,6 +136,11 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedObjectReadRuntime obje // null check on obj il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brfalse, nullLabel); + // Internal protocol probes also pass the emitted undefined sentinel. + // It has no symbol properties or prototype to traverse. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.UndefinedType); + il.Emit(OpCodes.Brtrue, nullLabel); // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) var notProxyLabel = il.DefineLabel(); @@ -284,6 +292,29 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedObjectReadRuntime obje var symbolFoundLabel = il.DefineLabel(); il.Emit(OpCodes.Brtrue, symbolFoundLabel); + // Definition-owned computed members live on the guest prototype, + // independently of the shared CLR template's symbol registry. + var prototype = il.DeclareLocal(_types.Object); + var prototypeLoop = il.DefineLabel(); + var noPrototype = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + il.MarkLabel(prototypeLoop); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Brfalse, noPrototype); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, inputs.Symbols.GetStorage); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, symbolValueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brtrue, symbolFoundLabel); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + il.Emit(OpCodes.Br, prototypeLoop); + il.MarkLabel(noPrototype); + // #266: symbol-keyed class accessor (`get [Symbol.x]() {...}`). After an // own symbol data property misses, consult the per-class accessor registry // (walking the base chain). A found getter is a MethodInfo invoked with the @@ -1411,6 +1442,7 @@ private void EmitSetIndex(TypeBuilder typeBuilder, EmittedObjectWriteRuntime obj // strict). il.MarkLabel(symbolKeyLabel); { + EmitInheritedSymbolSetter(il, inputs.ObjectPrototypes, inputs.Symbols, inputs.DescriptorStorage, inputs.InvokeMethodValue); // #266: symbol-keyed class accessor setter (`set [Symbol.x](v) {...}`). // A registered setter takes the write (accessor semantics) instead of // storing a data property. Found setter is a MethodInfo invoked with the @@ -1939,6 +1971,64 @@ void SkipIfMathConst(string n) il.Emit(OpCodes.Ret); } + private void EmitInheritedSymbolSetter(ILGenerator il, EmittedObjectPrototypeRuntime prototypes, + EmittedSymbolRuntime symbols, EmittedDescriptorStorageRuntime storage, MethodBuilder invoke) + { + var done = il.DefineLabel(); + var prototype = il.DeclareLocal(_types.Object); + var value = il.DeclareLocal(_types.Object); + var descriptor = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, symbols.GetStorage); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, value); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brtrue, done); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, prototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + var loop = il.DefineLabel(); + var next = il.DefineLabel(); + il.MarkLabel(loop); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Brfalse, done); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, symbols.GetStorage); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, value); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, value); + il.Emit(OpCodes.Isinst, storage.DescriptorType); + il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Brfalse, done); + var noSetter = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorSetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, noSetter); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorSetter.GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Call, invoke); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ret); + il.MarkLabel(noSetter); + il.Emit(OpCodes.Br, done); + il.MarkLabel(next); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, prototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + il.Emit(OpCodes.Br, loop); + il.MarkLabel(done); + } + /// /// Emits DeleteIndex(object obj, object key) -> bool /// Handles both symbol keys and string keys for delete operations. diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs index c9b58d705..ee3d18ea1 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs @@ -65,6 +65,7 @@ private LocalBuilder EmitStaticMemberLookupOwner(ILGenerator il, LocalBuilder ow } private readonly record struct GetPropertyInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedAbortRuntime? Abort, FieldBuilder ArgumentsLengthField, TypeBuilder ArgumentsType, @@ -86,6 +87,7 @@ private readonly record struct GetPropertyInputs( TypeBuilder FunctionApplyWrapperType, TypeBuilder FunctionBindWrapperType, TypeBuilder FunctionCallWrapperType, + EmittedFunctionAttributesRuntime FunctionAttributes, FieldBuilder FunctionPrototypeField, MethodBuilder FunctionPrototypePopulateMethod, MethodBuilder GetFunctionMethod, @@ -1370,6 +1372,35 @@ void EmitBoundRuntimeMethod(MethodBuilder helper, string jsName, int jsLength) il.Emit(OpCodes.Ret); il.MarkLabel(notGlobalThisLabel); + var notDefinitionValue = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notDefinitionValue); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ClassDefinitions.ReadProperty); + il.Emit(OpCodes.Ret); + il.MarkLabel(notDefinitionValue); + il.Emit(OpCodes.Pop); + + var notDefinitionConstructor = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notDefinitionConstructor); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Dup); + var noDefinitionReceiver = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, noDefinitionReceiver); + il.Emit(OpCodes.Callvirt, inputs.ClassDefinitions.GetDefinition); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, noDefinitionReceiver); + il.Emit(OpCodes.Ret); + il.MarkLabel(noDefinitionReceiver); + il.Emit(OpCodes.Pop); + il.MarkLabel(notDefinitionConstructor); + // __proto__ accessor (ECMA-262 Annex B.2.2.1): obj.__proto__ delegates // to Object.getPrototypeOf(obj). All object types support this — the // accessor lives on Object.prototype, but intercepting here avoids @@ -2440,6 +2471,24 @@ void EmitErrorTypeBranch(Type ctorType, MethodBuilder populate, FieldBuilder pro il.Emit(OpCodes.Ldstr, "BigInt"); il.Emit(OpCodes.Ret); il.MarkLabel(genericTypeNameLabel); + var reflectedTypeNameLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, typeLocal); + il.Emit(OpCodes.Ldtoken, inputs.FunctionAttributes.FunctionNameType); + il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsDefined", _types.Type, _types.Boolean)); + il.Emit(OpCodes.Brfalse, reflectedTypeNameLabel); + il.Emit(OpCodes.Ldloc, typeLocal); + il.Emit(OpCodes.Ldtoken, inputs.FunctionAttributes.FunctionNameType); + il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetCustomAttributes", _types.Type, _types.Boolean)); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, inputs.FunctionAttributes.FunctionNameType); + il.Emit(OpCodes.Ldfld, inputs.FunctionAttributes.FunctionNameValueField); + il.Emit(OpCodes.Ret); + il.MarkLabel(reflectedTypeNameLabel); il.Emit(OpCodes.Ldloc, typeLocal); il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "Name").GetGetMethod()!); il.Emit(OpCodes.Ret); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs index ffa1b31e7..00c470095 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs @@ -28,6 +28,7 @@ Type UndefinedType ); private readonly record struct ObjectGetPrototypeOfInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedBooleanRuntime Booleans, TypeBuilder BoundTSFunctionType, @@ -670,6 +671,19 @@ FieldBuilder prototypeStoreField // Also check local _prototypeStore table for backward compatibility il.MarkLabel(checkLocalTableLabel); + var notAssociatedInstance = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notAssociatedInstance); + il.Emit(OpCodes.Callvirt, inputs.ClassDefinitions.GetDefinition); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notAssociatedInstance); + il.Emit(OpCodes.Ldfld, inputs.ClassDefinitions.Prototype); + il.Emit(OpCodes.Ret); + il.MarkLabel(notAssociatedInstance); + il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldsfld, prototypeStoreField); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldloca, tempLocal); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs index f2f60a22d..d3522e696 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs @@ -54,6 +54,7 @@ MethodBuilder SafeGetMethod ); private readonly record struct SetPropertyInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedAbortRuntime? Abort, FieldBuilder ArgumentsLengthField, TypeBuilder ArgumentsType, @@ -87,6 +88,7 @@ Type UndefinedType ); private readonly record struct SetPropertyStrictInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedArrayStorageRuntime ArrayStorage, TypeBuilder BoundAnyFunctionType, @@ -966,6 +968,9 @@ private void EmitSetProperty(EmittedObjectWriteRuntime objectWrite, SetPropertyI // class-instance path that doesn't match $TSFunction. var tsFunctionSetLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brtrue, tsFunctionSetLabel); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, inputs.TSFunctionType); il.Emit(OpCodes.Brtrue, tsFunctionSetLabel); il.Emit(OpCodes.Ldarg_0); @@ -2044,6 +2049,9 @@ SetPropertyStrictInputs inputs // user-assigned properties through PDSDefineProperty). var tsFunctionSetStrictLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brtrue, tsFunctionSetStrictLabel); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, inputs.TSFunctionType); il.Emit(OpCodes.Brtrue, tsFunctionSetStrictLabel); il.Emit(OpCodes.Ldarg_0); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs b/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs index aebba6093..0e07c610a 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs @@ -9,6 +9,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct TypeOfInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, TypeBuilder BoundAnyFunctionType, TypeBuilder BoundTSFunctionType, @@ -26,6 +27,8 @@ Type UndefinedType ); private readonly record struct InstanceOfInputs( + EmittedClassDefinitionRuntime ClassDefinitions, + EmittedClassPrototypeRuntime ClassPrototypes, EmittedAbortRuntime? Abort, EmittedBoxedPrimitiveRuntime BoxedPrimitives, MethodBuilder GetFunctionMethod, @@ -553,6 +556,9 @@ private void EmitTypeOf(TypeBuilder typeBuilder, EmittedOperatorRuntime operator il.Emit(OpCodes.Brtrue, functionLabel); // System.Type => "function" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brtrue, functionLabel); // Compiled class references (e.g. `const f = Foo` where Foo is a class) are // emitted as Ldtoken + GetTypeFromHandle, which yields a System.Type. Node/JS // spec says classes are functions, so `typeof Foo === 'function'` must hold. @@ -687,6 +693,31 @@ private void EmitInstanceOf(TypeBuilder typeBuilder, EmittedOperatorRuntime oper il.MarkLabel(notDictRhsLabel); + var notDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brfalse, notDefinition); + var definitionPrototype = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, inputs.ClassDefinitions.Prototype); + il.Emit(OpCodes.Stloc, definitionPrototype); + var candidate = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stloc, candidate); + var walk = il.DefineLabel(); + il.MarkLabel(walk); + il.Emit(OpCodes.Ldloc, candidate); + il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, candidate); + il.Emit(OpCodes.Ldloc, candidate); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldloc, candidate); + il.Emit(OpCodes.Ldloc, definitionPrototype); + il.Emit(OpCodes.Beq, trueLabel); + il.Emit(OpCodes.Br, walk); + il.MarkLabel(notDefinition); + // Per JS spec, `instance instanceof F` where F is a user function walks // instance's prototype chain looking for F.prototype. Compiled mode's // legacy InstanceOf used .NET IsAssignableFrom, which is type-system @@ -761,6 +792,26 @@ private void EmitInstanceOf(TypeBuilder typeBuilder, EmittedOperatorRuntime oper il.Emit(OpCodes.Castclass, _types.Type); il.Emit(OpCodes.Stloc, classTypeLocal); + // A guest class-definition receiver can inherit a selected user template + // without CLR assignability. Consult its ordinary prototype chain first. + var staticReceiver = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Brfalse, staticReceiver); + var selectedPrototype = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Call, inputs.ClassPrototypes.Get); il.Emit(OpCodes.Stloc, selectedPrototype); + var selectedCandidate = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Stloc, selectedCandidate); + var selectedWalk = il.DefineLabel(); + il.MarkLabel(selectedWalk); + il.Emit(OpCodes.Ldloc, selectedCandidate); il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, selectedCandidate); + il.Emit(OpCodes.Ldloc, selectedCandidate); il.Emit(OpCodes.Brfalse, staticReceiver); + il.Emit(OpCodes.Ldloc, selectedCandidate); il.Emit(OpCodes.Ldloc, selectedPrototype); il.Emit(OpCodes.Beq, trueLabel); + il.Emit(OpCodes.Br, selectedWalk); + il.MarkLabel(staticReceiver); + // `x instanceof Object` — the bare `Object` identifier resolves to the // System.Object Type token (see RuntimeEmitter.GlobalThis.cs), so apply // JS Object semantics rather than the IsAssignableFrom fallback below: diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs b/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs index d068b3208..a03028e55 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct IsConstructorInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedFunctionBindingRuntime FunctionBindings, EmittedFunctionValueRuntime FunctionValues, EmittedFunctionAttributesRuntime FunctionAttributes, @@ -1068,6 +1069,14 @@ IsConstructorInputs inputs il.Emit(OpCodes.Ret); il.MarkLabel(notTypeLabel); + var notDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brfalse, notDefinition); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + il.MarkLabel(notDefinition); + // $TSFunction → check method's declaring type var notTSFunctionLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 9694c91f9..38080c760 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -768,6 +768,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.Operators, new TypeOfInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.FunctionBindings.AnyType, runtime.FunctionBindings.BoundType, @@ -919,6 +920,8 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.Operators, new InstanceOfInputs( + runtime.ClassDefinitions, + runtime.ClassPrototypes, runtime.Abort, runtime.BoxedPrimitives, runtime.FunctionIntrospection.GetProperty, @@ -1128,6 +1131,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.FunctionIntrospection, new IsConstructorInputs( + runtime.ClassDefinitions, runtime.FunctionBindings, runtime.FunctionValues, runtime.FunctionAttributes, @@ -1194,6 +1198,7 @@ void EmitLinkProto(FieldBuilder child) EmitGetProperty( runtime.ObjectRead, new GetPropertyInputs( + runtime.ClassDefinitions, runtime.Abort, runtime.Arguments.LengthField, runtime.Arguments.Type, @@ -1215,6 +1220,7 @@ void EmitLinkProto(FieldBuilder child) runtime.FunctionBindings.ApplyType, runtime.FunctionBindings.BindType, runtime.FunctionBindings.CallType, + runtime.FunctionAttributes, runtime.FunctionPrototypes.Prototype, runtime.FunctionPrototypes.Populate, runtime.FunctionIntrospection.GetProperty, @@ -1258,6 +1264,7 @@ void EmitLinkProto(FieldBuilder child) runtime.WeakSet ) ); + EmitClassDefinitionBodies(runtime); // UnwrapIfBoxed's body is filled after GetIndex below. In addition to // GetProperty/InvokeMethodValue it now uses indexed symbol lookup for // the @@toPrimitive hook. @@ -1320,6 +1327,7 @@ void EmitLinkProto(FieldBuilder child) EmitSetProperty( runtime.ObjectWrite, new SetPropertyInputs( + runtime.ClassDefinitions, runtime.Abort, runtime.Arguments.LengthField, runtime.Arguments.Type, @@ -1356,6 +1364,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.ObjectWrite, new SetPropertyStrictInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.ArrayStorage, runtime.FunctionBindings.AnyType, @@ -1470,6 +1479,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.ObjectRead, new GetIndexInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.ArrayStorage, runtime.FunctionBindings.BoundType, @@ -1483,6 +1493,7 @@ void EmitLinkProto(FieldBuilder child) runtime.ReflectedMethods.InvokeUnwrapped, runtime.Invocation.Method, runtime.ObjectDescriptors, + runtime.ObjectPrototypes, runtime.ObjectStorage, runtime.RegExps, runtime.StringCoercion, @@ -1508,6 +1519,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.ObjectWrite, new SetIndexInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.ArrayStorage, runtime.Buffer, @@ -1984,6 +1996,7 @@ void EmitLinkProto(FieldBuilder child) runtime.ClassPrototypes, runtime.ObjectPrototypes, new ObjectGetPrototypeOfInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.Booleans, runtime.FunctionBindings.BoundType, diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs b/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs index cdcfbe882..a5acf0dfb 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs @@ -17,7 +17,7 @@ private void EmitGetSymbolDict(TypeBuilder typeBuilder, EmittedSymbolRuntime sym var method = typeBuilder.DefineMethod( "GetSymbolDict", - MethodAttributes.Private | MethodAttributes.Static, + MethodAttributes.Assembly | MethodAttributes.Static, symbolDictType, [_types.Object] ); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.cs b/src/SharpTS/Compilation/RuntimeEmitter.cs index aa0d8edcb..4c0bbaece 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.cs @@ -308,6 +308,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea EmitHasFieldsInterface(moduleBuilder, runtime.ObjectFields); runtime.ObjectFields.CompleteEmission(); EmitCompactObjectRecordInterface(moduleBuilder, runtime.Records); + DefineClassDefinitionTypes(moduleBuilder, runtime.ClassDefinitions); // Emit $Object class for standalone object support // NOTE: Must stay in sync with SharpTS.Runtime.Types.SharpTSObject @@ -609,6 +610,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea moduleBuilder, runtime.FunctionBindings, new FunctionBindWrapperClassInputs( + runtime.ClassDefinitions, runtime.FunctionValues, runtime.ArrayOperations, runtime.Map, @@ -698,6 +700,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea runtime.RuntimeClass.Type, runtime.DynamicConstruction, new NewOnFunctionInputs( + runtime.ClassDefinitions, runtime.DescriptorStorage, runtime.Errors, runtime.FunctionBindings, @@ -716,6 +719,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea runtime.RuntimeClass.Type, runtime.DynamicConstruction, new ConstructDynamicValueInputs( + runtime.ClassDefinitions, runtime.BoxedPrimitives, runtime.Errors, runtime.FunctionConstruction, diff --git a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs index 9095b8b8b..1e0b0612b 100644 --- a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs +++ b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs @@ -91,6 +91,7 @@ public RuntimeFeatureDetector() UsesReflect = false, UsesIteratorHelpers = false, UsesPromise = false, + UsesUserClasses = false, UsesDate = false, UsesRegExp = false, UsesBuffer = false, @@ -766,6 +767,7 @@ private void VisitStmt(Stmt? stmt) break; case Stmt.Class cls: + _set.UsesUserClasses = true; foreach (var expression in ClassDefinitionExpressions.Enumerate(cls)) VisitExpr(expression); foreach (var m in cls.Methods) @@ -1589,6 +1591,9 @@ private void VisitExpr(Expr? expr) case Expr.GetPrivate gp: VisitExpr(gp.Object); break; + case Expr.PrivateIn presence: + VisitExpr(presence.Object); + break; case Expr.SetPrivate sp: VisitExpr(sp.Object); VisitExpr(sp.Value); @@ -1697,6 +1702,7 @@ prop.Key is not Expr.ComputedKey && VisitExpr(nn.Expression); break; case Expr.ClassExpr ce: + _set.UsesUserClasses = true; foreach (var expression in ClassDefinitionExpressions.Enumerate(ce)) VisitExpr(expression); foreach (var m in ce.Methods) diff --git a/src/SharpTS/Compilation/RuntimeFeatureSet.cs b/src/SharpTS/Compilation/RuntimeFeatureSet.cs index 1b0e0bd5e..8fbc7d4e9 100644 --- a/src/SharpTS/Compilation/RuntimeFeatureSet.cs +++ b/src/SharpTS/Compilation/RuntimeFeatureSet.cs @@ -126,6 +126,7 @@ internal bool CanAssumeCompactObjectRecordIsUnmaterialized(string fingerprint) = public bool UsesReflect { get; set; } = true; // Reflect.set/get/deleteProperty/has/etc. public bool UsesIteratorHelpers { get; set; } = true; // Iterator.prototype.map/filter/flatMap/take/drop public bool UsesPromise { get; set; } = true; // Promise references, async/await, and Promise-returning host/module surfaces + public bool UsesUserClasses { get; set; } = true; // user-class receiver initialization and guest prototype lookup public bool UsesDate { get; set; } = true; // new Date(), Date.now(), Date.X public bool UsesRegExp { get; set; } = true; // /pattern/ or new RegExp() public bool UsesBuffer { get; set; } = true; // Buffer.from(), new Buffer() — also implied by crypto/fs/zlib/http/fetch/dgram diff --git a/src/SharpTS/Compilation/StatementEmitterBase.cs b/src/SharpTS/Compilation/StatementEmitterBase.cs index bef25f6bc..2e77f31ba 100644 --- a/src/SharpTS/Compilation/StatementEmitterBase.cs +++ b/src/SharpTS/Compilation/StatementEmitterBase.cs @@ -1826,6 +1826,23 @@ protected virtual void EmitStoreLoopVariable(LocalBuilder? local, string name, A private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) { + if (Ctx.RuntimeClassDeclarations?.TryGetValue(classStmt, out var runtimeExpression) == true) + { + EmitGuestClassDefinition(runtimeExpression); + var definition = IL.DeclareLocal(Types.Object); + IL.Emit(OpCodes.Stloc, definition); + string name = GetClassStorageName(classStmt); + if (GetHoistedVariableField(name) is { } binding) + { + IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldloc, definition); IL.Emit(OpCodes.Stfld, binding); + } + else + { + var definitionLocal = Ctx.Locals.GetLocal(name) ?? Ctx.Locals.DeclareLocal(name, Types.Object, classStmt); + IL.Emit(OpCodes.Ldloc, definition); IL.Emit(OpCodes.Stloc, definitionLocal); + } + return; + } TypeBuilder? builder = null; if (Ctx.BlockScopedClassBuilders?.TryGetValue(classStmt, out var scopedBuilder) == true) builder = scopedBuilder; @@ -1867,58 +1884,13 @@ private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) /// values to as an object array. /// protected void EmitDeferredComputedKeys(MethodBuilder method, IReadOnlyList keys) - { - var values = new List(keys.Count); - foreach (var key in keys) - { - EmitExpression(key); - EnsureBoxed(); - var isSymbol = IL.DefineLabel(); - IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); - IL.Emit(OpCodes.Brtrue, isSymbol); - IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); - IL.MarkLabel(isSymbol); - values.Add(_helpers.SpillStoreObject()); - } - - IL.Emit(OpCodes.Ldc_I4, values.Count); - IL.Emit(OpCodes.Newarr, Types.Object); - for (int i = 0; i < values.Count; i++) - { - IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Ldc_I4, i); - IL.Emit(OpCodes.Ldloc, values[i]); - IL.Emit(OpCodes.Stelem_Ref); - } - IL.Emit(OpCodes.Call, method); - } + => EmitDefinitionComputedKeys(method, keys); /// /// Default implementation for class expressions. /// Loads the pre-defined TypeBuilder as a Type object at runtime. /// - protected override void EmitClassExpression(Expr.ClassExpr ce) - { - if (Ctx?.ClassExprBuilders != null && Ctx.ClassExprBuilders.TryGetValue(ce, out var typeBuilder)) - { - EmitClassHeritageExpression(ce.SuperclassExpr, ce.Name?.Lexeme); - IL.Emit(OpCodes.Ldtoken, typeBuilder); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassInitialization.RunDefinition); - if (Ctx.DeferredClassDefinitions?.TryGet(ce, out var deferred) == true) - EmitDeferredComputedKeys(deferred.Registrar, deferred.Keys); - IL.Emit(OpCodes.Ldtoken, typeBuilder); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - SetStackUnknown(); - } - else - { - // Fallback: push null (should not happen if collection worked) - IL.Emit(OpCodes.Ldnull); - SetStackUnknown(); - } - } + protected override void EmitClassExpression(Expr.ClassExpr ce) => EmitGuestClassDefinition(ce); #endregion diff --git a/src/SharpTS/Compilation/TypeMapper.cs b/src/SharpTS/Compilation/TypeMapper.cs index 8c0be8a8b..98642a047 100644 --- a/src/SharpTS/Compilation/TypeMapper.cs +++ b/src/SharpTS/Compilation/TypeMapper.cs @@ -20,6 +20,12 @@ public class TypeMapper { private readonly ModuleBuilder _moduleBuilder; private readonly TypeProvider _types; + private readonly Dictionary _checkedClassNames = []; + + internal void RegisterCheckedClassName(int declarationId, string name) => _checkedClassNames[declarationId] = name; + + private Type GetCheckedClassType(ClassMetadataCore core) => GetClassType( + _checkedClassNames.GetValueOrDefault(core.DeclarationId, core.Name)); private Dictionary? _classBuilders; private UnionTypeGenerator? _unionGenerator; public ExternalTypeRegistry ExternalTypeDeclarations { get; } = new(); @@ -149,10 +155,10 @@ private void RequireLocalClass(TypeBuilder builder) TypeInfo.Array => _types.Object, // Will be TSArray at runtime TypeInfo.Function => _types.Object, // Will be delegate at runtime TypeInfo.Promise p => MapPromiseType(p), // Promise maps to Task - TypeInfo.Class c => GetClassType(c.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), TypeInfo.Instance i => i.ClassType switch { - TypeInfo.Class c => GetClassType(c.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), TypeInfo.InstantiatedGeneric => _types.Object, _ => _types.Object }, @@ -233,7 +239,7 @@ public Type GetClassType(string className) TypeInfo.Array arr => MapArrayTypeStrict(arr), TypeInfo.Function => _types.Delegate, // Functions map to Delegate for typed interop TypeInfo.Promise p => MapPromiseTypeStrict(p), - TypeInfo.Class c => GetClassType(c.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), TypeInfo.Instance i => MapInstanceTypeStrict(i), TypeInfo.Record => _types.Object, // Records remain dynamic objects TypeInfo.Void => _types.Void, @@ -253,7 +259,7 @@ public Type GetClassType(string className) TypeInfo.Symbol => _types.String, // Symbols map to string keys // Generic types - attempt to resolve if instantiated TypeInfo.TypeParameter => _types.Object, - TypeInfo.GenericClass gc => GetClassType(gc.Name), + TypeInfo.GenericClass gc => GetCheckedClassType(gc.Core), TypeInfo.GenericFunction => _types.Delegate, TypeInfo.GenericInterface => _types.Object, TypeInfo.InstantiatedGeneric ig => MapInstantiatedGenericStrict(ig), @@ -268,8 +274,8 @@ public Type GetClassType(string className) private Type MapInstanceTypeStrict(TypeInfo.Instance instance) => instance.ClassType switch { - TypeInfo.Class c => GetClassType(c.Name), - TypeInfo.GenericClass gc => GetClassType(gc.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), + TypeInfo.GenericClass gc => GetCheckedClassType(gc.Core), TypeInfo.InstantiatedGeneric ig => MapInstantiatedGenericStrict(ig), _ => _types.Object }; @@ -278,7 +284,7 @@ private Type MapInstantiatedGenericStrict(TypeInfo.InstantiatedGeneric ig) { // For instantiated generics, try to resolve the base type if (ig.GenericDefinition is TypeInfo.GenericClass gc) - return GetClassType(gc.Name); + return GetCheckedClassType(gc.Core); return _types.Object; } diff --git a/src/SharpTS/Execution/Interpreter.Calls.cs b/src/SharpTS/Execution/Interpreter.Calls.cs index 0e58ee8a1..98c2696ec 100644 --- a/src/SharpTS/Execution/Interpreter.Calls.cs +++ b/src/SharpTS/Execution/Interpreter.Calls.cs @@ -1252,7 +1252,7 @@ private object EvaluateInstanceof(object? left, object? right) SharpTSClass? current = instance.GetClass(); while (current != null) { - if (current.Name == targetClass.Name) return true; + if (ReferenceEquals(current, targetClass)) return true; current = current.Superclass; } return false; @@ -1266,7 +1266,7 @@ private object EvaluateInstanceof(object? left, object? right) SharpTSClass? current = subclassArray.Klass; while (current != null) { - if (current.Name == targetClass.Name) return true; + if (ReferenceEquals(current, targetClass)) return true; current = current.Superclass; } return false; @@ -1280,7 +1280,7 @@ private object EvaluateInstanceof(object? left, object? right) SharpTSClass? current = subclassPromise.Klass; while (current != null) { - if (current.Name == targetClass.Name) return true; + if (ReferenceEquals(current, targetClass)) return true; current = current.Superclass; } return false; diff --git a/src/SharpTS/Execution/Interpreter.ClassInitializers.cs b/src/SharpTS/Execution/Interpreter.ClassInitializers.cs new file mode 100644 index 000000000..669f7c2d7 --- /dev/null +++ b/src/SharpTS/Execution/Interpreter.ClassInitializers.cs @@ -0,0 +1,19 @@ +using SharpTS.Parsing; +using SharpTS.Runtime; +using SharpTS.Runtime.Types; + +namespace SharpTS.Execution; + +public partial class Interpreter +{ + internal object? EvaluateClassInitializer(SharpTSClass owner, object receiver, Expr expression) + { + var environment = new RuntimeEnvironment(owner.InitializerEnvironment ?? _environment) + { + PrivateClass = owner + }; + environment.Define("this", receiver); + if (owner.Superclass is { } parent) environment.Define("super", parent); + using (PushScope(environment)) return Evaluate(expression); + } +} diff --git a/src/SharpTS/Execution/Interpreter.Dispatch.cs b/src/SharpTS/Execution/Interpreter.Dispatch.cs index 802750f9e..b725d09ae 100644 --- a/src/SharpTS/Execution/Interpreter.Dispatch.cs +++ b/src/SharpTS/Execution/Interpreter.Dispatch.cs @@ -52,6 +52,7 @@ public partial class Interpreter Expr.DestructuringAssign e => VisitDestructuringAssign(e), Expr.Delete e => VisitDelete(e), Expr.GetPrivate e => VisitGetPrivate(e), + Expr.PrivateIn e => VisitPrivateIn(e), Expr.SetPrivate e => VisitSetPrivate(e), Expr.CallPrivate e => VisitCallPrivate(e), Expr.Super e => VisitSuper(e), @@ -158,6 +159,7 @@ public partial class Interpreter Expr.DestructuringAssign e => VisitDestructuringAssignAsync(e), Expr.Delete e => VisitDeleteAsync(e), Expr.GetPrivate e => VisitGetPrivateAsync(e), + Expr.PrivateIn e => VisitPrivateInAsync(e), Expr.SetPrivate e => VisitSetPrivateAsync(e), Expr.CallPrivate e => VisitCallPrivateAsync(e), Expr.Super e => VisitSuperAsync(e), diff --git a/src/SharpTS/Execution/Interpreter.Expressions.cs b/src/SharpTS/Execution/Interpreter.Expressions.cs index 6de7dbf39..406c02bb2 100644 --- a/src/SharpTS/Execution/Interpreter.Expressions.cs +++ b/src/SharpTS/Execution/Interpreter.Expressions.cs @@ -2073,7 +2073,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon throw new InterpreterException( $"Class expression cannot extend built-in '{builtInCtor.Name}': subclassing this built-in is not supported yet."); } - throw new InterpreterException("Superclass must be a class."); + throw new ThrowException(new SharpTSTypeError("Superclass must be a class constructor.")); } } @@ -2099,6 +2099,10 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon Dictionary staticMethods = []; Dictionary staticProperties = []; List instanceFields = []; + List instancePrivateFields = []; + Dictionary privateMethods = []; + Dictionary staticPrivateMethods = []; + Dictionary staticPrivateFields = []; // Check if we have static initializers for proper ordering bool hasStaticInitializers = classExpr.StaticInitializers != null && classExpr.StaticInitializers.Count > 0; @@ -2115,7 +2119,13 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon foreach (Stmt.Field field in classExpr.Fields) { - if (field.IsStatic) + if (field.IsPrivate) + { + if (!field.IsStatic) instancePrivateFields.Add(field); + else staticPrivateFields[field.Name.Lexeme] = !hasStaticInitializers && field.Initializer != null + ? Evaluate(field.Initializer) : SharpTSUndefined.Instance; + } + else if (field.IsStatic) { if (!hasStaticInitializers) { @@ -2166,7 +2176,9 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon continue; } - if (method.IsStatic) + if (method.IsPrivate) + (method.IsStatic ? staticPrivateMethods : privateMethods)[method.Name.Lexeme] = func; + else if (method.IsStatic) { staticMethods[method.Name.Lexeme] = func; } @@ -2237,7 +2249,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null) + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods) : superclass is SharpTSArrayClass arraySuper ? new SharpTSArrayClass( className, @@ -2250,7 +2262,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null) + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods) : superclass is SharpTSPromiseClass promiseSuper ? new SharpTSPromiseClass( className, @@ -2263,7 +2275,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null) + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods) : new SharpTSClass( className, (SharpTSClass?)superclass, @@ -2275,9 +2287,13 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null); + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods); klass.BindPrivateMemberOwners(); + klass.InitializerEnvironment = classEnv; + klass.OrderedInstanceFields = classExpr.Fields.Where(field => !field.IsStatic) + .SelectMany(field => (field.IsPrivate ? instancePrivateFields : instanceFields) + .Where(captured => captured.Name == field.Name)).ToList(); if (symbolAccessors != null) { @@ -2300,7 +2316,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon { // Create temporary environment with 'this' bound to the class // Also make the class name available so code like Foo.x works - var staticEnv = new RuntimeEnvironment(_environment); + var staticEnv = new RuntimeEnvironment(_environment, strictMode: true) { PrivateClass = klass }; staticEnv.Define("this", klass); if (classExpr.Name != null) { @@ -2317,7 +2333,8 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon object? fieldValue = field.Initializer != null ? Evaluate(field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; - if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) + if (field.IsPrivate) klass.InstallStaticPrivateField(field.Name.Lexeme, fieldValue); + else if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) klass.SetStaticBySymbol(symbol, fieldValue); else klass.SetStaticProperty(field.ComputedKey != null diff --git a/src/SharpTS/Execution/Interpreter.Namespaces.cs b/src/SharpTS/Execution/Interpreter.Namespaces.cs index 462f8eb99..57f5a30c2 100644 --- a/src/SharpTS/Execution/Interpreter.Namespaces.cs +++ b/src/SharpTS/Execution/Interpreter.Namespaces.cs @@ -47,6 +47,10 @@ private ExecutionResult ExecuteNamespace(Stmt.Namespace ns) { namespaceEnv.DefineNamespace(memberName, nestedNs); } + else + { + namespaceEnv.Define(memberName, memberValue); + } } } diff --git a/src/SharpTS/Execution/Interpreter.PrivateIn.cs b/src/SharpTS/Execution/Interpreter.PrivateIn.cs new file mode 100644 index 000000000..af92c5989 --- /dev/null +++ b/src/SharpTS/Execution/Interpreter.PrivateIn.cs @@ -0,0 +1,30 @@ +using SharpTS.Parsing; +using SharpTS.Runtime; +using SharpTS.Runtime.Exceptions; +using SharpTS.Runtime.Types; + +namespace SharpTS.Execution; + +public partial class Interpreter +{ + internal RuntimeValue VisitPrivateIn(Expr.PrivateIn expr) + => PrivateInCore(Evaluate(expr.Object), expr.Name.Lexeme); + + internal async ValueTask VisitPrivateInAsync(Expr.PrivateIn expr) + => PrivateInCore((await EvaluateAsync(expr.Object)).ToObject(), expr.Name.Lexeme); + + private RuntimeValue PrivateInCore(object? receiver, string name) + { + if (receiver is null or SharpTSUndefined or string or double or bool or SharpTSBigInt or SharpTSSymbol) + throw new ThrowException(new SharpTSTypeError("Right-hand side of private 'in' is not an object")); + var owner = _environment.FindPrivateClass(name) + ?? throw new InterpreterException("Private name has no lexical class owner"); + return RuntimeValue.FromBoolean(owner.HasStaticPrivateField(name) + ? ReferenceEquals(receiver, owner) && owner.HasInstalledStaticPrivateField(name) + : owner.GetStaticPrivateMethod(name) != null + ? ReferenceEquals(receiver, owner) + : owner.GetPrivateMethod(name) != null + ? owner.HasPrivateBrand(receiver) + : owner.HasInstalledPrivateField(receiver, name)); + } +} diff --git a/src/SharpTS/Execution/Interpreter.Properties.cs b/src/SharpTS/Execution/Interpreter.Properties.cs index ef1168ddf..1bb03f6f1 100644 --- a/src/SharpTS/Execution/Interpreter.Properties.cs +++ b/src/SharpTS/Execution/Interpreter.Properties.cs @@ -128,7 +128,7 @@ private static SharpTSObject CreateConstructedThis(object? prototype) // binding `this` to a fresh object whose prototype is Foo.prototype. // Without this, `self instanceof Yallist` is false and packages // recurse infinitely. - if (klass is SharpTSFunction userFn) + if (klass is SharpTSFunction { IsPrivateMethod: false } userFn) { // Build a new `this` object backed by the function's prototype. if (!userFn.TryGetProperty("prototype", out var protoObj)) @@ -362,7 +362,7 @@ private RuntimeValue EvaluateNew(Expr.New newExpr) // Build a fresh `this` with prototype linkage, bind it, then call — // so `this instanceof Foo` returns true (Node CJS pattern used by // e.g. yallist, EventEmitter sub-classes). - if (klass is SharpTSFunction userFn) + if (klass is SharpTSFunction { IsPrivateMethod: false } userFn) { if (!userFn.TryGetProperty("prototype", out var protoObj)) { @@ -1552,6 +1552,7 @@ private static object BindBuiltInMember(object member, object receiver) /// internal static bool IsNonConstructorWrapper(object? callable) => callable is ISharpTSNonConstructorCallable + or SharpTSFunction { IsPrivateMethod: true } or SharpTSArrowFunction { HasOwnThis: false } or SharpTSAsyncFunction or SharpTSAsyncArrowFunction @@ -1624,6 +1625,8 @@ or BuiltInAsyncMethod // shadowed them with a static property (handled above). if (memberName == "name") return klass.Name; if (memberName == "length") return (double)klass.Arity(); + if (memberName is "bind" or "call" or "apply") + return FunctionBuiltIns.GetMember(klass, memberName); // Class constructors are function objects and therefore inherit the // ordinary Object.prototype methods through Function.prototype. @@ -3100,6 +3103,24 @@ private async Task EvaluateGetPrivateAsync(Expr.GetPrivate expr) private RuntimeValue GetPrivateCore(object? obj, string fieldName) { + var owner = _environment.PrivateClass; + if (owner?.HasStaticPrivateField(fieldName) == true) + { + if (!ReferenceEquals(obj, owner)) throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + return owner.GetStaticPrivateFieldRV(fieldName); + } + if (owner?.GetPrivateMethod(fieldName) is { } privateMethod) + { + if (obj == null || !owner.HasPrivateBrand(obj)) + throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + return RuntimeValue.FromBoxed(privateMethod); + } + if (owner?.GetStaticPrivateMethod(fieldName) is { } staticMethod) + { + if (!ReferenceEquals(obj, owner)) + throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + return RuntimeValue.FromBoxed(staticMethod); + } // Handle static private field access on class if (obj is SharpTSClass klass) { @@ -3120,7 +3141,7 @@ private RuntimeValue GetPrivateCore(object? obj, string fieldName) return declaringClass.GetPrivateFieldRV(instance, fieldName); } - throw new InterpreterException($"Cannot read private field '{fieldName}' from non-class value."); + throw new ThrowException(new SharpTSTypeError($"Cannot read private field '{fieldName}' from non-class value.")); } /// @@ -3143,6 +3164,12 @@ private async Task EvaluateSetPrivateAsync(Expr.SetPrivate expr) private RuntimeValue SetPrivateCore(object? obj, RuntimeValue value, string fieldName) { + if (_environment.PrivateClass is { } owner && owner.HasStaticPrivateField(fieldName)) + { + if (!ReferenceEquals(obj, owner)) throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + owner.SetStaticPrivateField(fieldName, value.ToObject()); + return value; + } // Handle static private field assignment on class if (obj is SharpTSClass klass) { @@ -3213,7 +3240,7 @@ private RuntimeValue CallPrivateCore(object? obj, List arguments, strin throw new InterpreterException($"Static private method '{methodName}' does not exist on class '{klass.Name}'."); } - return RuntimeValue.FromBoxed(method.CallBoxed(this, arguments)); + return RuntimeValue.FromBoxed(SharpTSClass.BindStaticMethod(method, klass).CallBoxed(this, arguments)); } // Instance private method call diff --git a/src/SharpTS/Execution/Interpreter.Statements.cs b/src/SharpTS/Execution/Interpreter.Statements.cs index f2f548874..fcb52de84 100644 --- a/src/SharpTS/Execution/Interpreter.Statements.cs +++ b/src/SharpTS/Execution/Interpreter.Statements.cs @@ -2030,7 +2030,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, throw new InterpreterException( $"Class '{classStmt.Name.Lexeme}' cannot extend built-in '{builtInCtor.Name}': subclassing this built-in is not supported yet."); } - throw new InterpreterException("Superclass must be a class."); + throw new ThrowException(new SharpTSTypeError("Superclass must be a class constructor.")); } } @@ -2081,7 +2081,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, : null; staticPrivateFields[field.Name.Lexeme] = fieldValue; } - // else: will be evaluated via StaticInitializers with proper 'this' binding + else staticPrivateFields[field.Name.Lexeme] = SharpTSUndefined.Instance; } else { @@ -2328,6 +2328,10 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, staticSetters.Count > 0 ? staticSetters : null); klass.BindPrivateMemberOwners(); + klass.InitializerEnvironment = _environment; + klass.OrderedInstanceFields = classStmt.Fields.Where(field => !field.IsStatic) + .SelectMany(field => (field.IsPrivate ? instancePrivateFields : instanceFields) + .Where(captured => captured.Name == field.Name)).ToList(); if (symbolAccessors != null) { @@ -2350,7 +2354,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, { // Create temporary environment with 'this' bound to the class // Also make the class name available so code like Foo.x works - var staticEnv = new RuntimeEnvironment(_environment); + var staticEnv = new RuntimeEnvironment(_environment, strictMode: true) { PrivateClass = klass }; staticEnv.Define("this", klass); staticEnv.Define(classStmt.Name.Lexeme, klass); @@ -2368,7 +2372,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, ? Evaluate(field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; if (field.IsPrivate) - klass.SetStaticPrivateField(field.Name.Lexeme, fieldValue); + klass.InstallStaticPrivateField(field.Name.Lexeme, fieldValue); else if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) klass.SetStaticBySymbol(symbol, fieldValue); else diff --git a/src/SharpTS/Execution/Interpreter.cs b/src/SharpTS/Execution/Interpreter.cs index ba6dcd460..36e751e40 100644 --- a/src/SharpTS/Execution/Interpreter.cs +++ b/src/SharpTS/Execution/Interpreter.cs @@ -2226,6 +2226,7 @@ private string GetDeclaredName(Stmt decl) // `export const x = …` now parses as Stmt.Const (was Stmt.Var before #428). Stmt.Const c => c.Name.Lexeme, Stmt.Enum e => e.Name.Lexeme, + Stmt.Namespace n => n.Name.Lexeme, _ => throw new InterpreterException($"Cannot get name of declaration type {decl.GetType().Name}") }; } @@ -2244,6 +2245,7 @@ private string GetDeclaredName(Stmt decl) // `export const x = …` now parses as Stmt.Const (was Stmt.Var before #428). Stmt.Const c => c.Name, Stmt.Enum e => e.Name, + Stmt.Namespace n => n.Name, _ => throw new InterpreterException($"Cannot get value of declaration type {decl.GetType().Name}") }; return _environment.Get(token).ToObject(); diff --git a/src/SharpTS/Parsing/AST.cs b/src/SharpTS/Parsing/AST.cs index a7ba991e4..3dbc6903a 100644 --- a/src/SharpTS/Parsing/AST.cs +++ b/src/SharpTS/Parsing/AST.cs @@ -115,6 +115,8 @@ public record Get(Expr Object, Token Name, bool Optional = false, bool Defaulted public record Set(Expr Object, Token Name, Expr Value) : Expr; /// Private field access: obj.#field public record GetPrivate(Expr Object, Token Name) : Expr; + /// Lexical private-name presence check: #name in object. + public record PrivateIn(Token Name, Expr Object) : Expr; /// Private field assignment: obj.#field = value public record SetPrivate(Expr Object, Token Name, Expr Value) : Expr; /// Private method call: obj.#method(args) diff --git a/src/SharpTS/Parsing/GeneratorArrowLifter.cs b/src/SharpTS/Parsing/GeneratorArrowLifter.cs index 06a876ba8..7d1b27fd7 100644 --- a/src/SharpTS/Parsing/GeneratorArrowLifter.cs +++ b/src/SharpTS/Parsing/GeneratorArrowLifter.cs @@ -706,6 +706,11 @@ private Expr RewriteExpr(Expr expr) var o = RewriteExpr(g.Object); return ReferenceEquals(o, g.Object) ? g : new Expr.Get(o, g.Name, g.Optional); } + case Expr.PrivateIn probe: + { + var o = RewriteExpr(probe.Object); + return ReferenceEquals(o, probe.Object) ? probe : probe with { Object = o }; + } case Expr.GetPrivate gp: { var o = RewriteExpr(gp.Object); diff --git a/src/SharpTS/Parsing/Parser.Expressions.cs b/src/SharpTS/Parsing/Parser.Expressions.cs index f177849ec..a385b2163 100644 --- a/src/SharpTS/Parsing/Parser.Expressions.cs +++ b/src/SharpTS/Parsing/Parser.Expressions.cs @@ -287,7 +287,14 @@ private Expr Equality() private Expr Comparison() { - Expr expr = Shift(); + Expr expr; + if (Match(TokenType.PRIVATE_IDENTIFIER)) + { + Token name = Previous(); + Consume(TokenType.IN, "Private identifier must be followed by 'in'."); + expr = new Expr.PrivateIn(name, Shift()); + } + else expr = Shift(); // In TSX, adjacent JSX roots are not a less-than comparison. tsc commits to both // elements, reports TS2657 on the first root, and retains both ASTs so their normal diff --git a/src/SharpTS/Parsing/Parser.ForInitializerValidation.cs b/src/SharpTS/Parsing/Parser.ForInitializerValidation.cs new file mode 100644 index 000000000..c277a7bf8 --- /dev/null +++ b/src/SharpTS/Parsing/Parser.ForInitializerValidation.cs @@ -0,0 +1,64 @@ +using SharpTS.Parsing.Visitors; + +namespace SharpTS.Parsing; + +public partial class Parser +{ + // Traditional for initializers use the NoIn grammar. Preserve the parser's + // existing ordinary `in` behavior while enforcing it for the new private-name + // production. These overrides stop at subexpressions whose grammar enables In. + private sealed class PrivateInForInitializerValidator : AstVisitorBase + { + protected override void VisitPrivateIn(Expr.PrivateIn expr) + => throw new Exception("Private identifier 'in' expressions in for-loop initializers must be parenthesized."); + + protected override void VisitGrouping(Expr.Grouping expr) { } + protected override void VisitArrayLiteral(Expr.ArrayLiteral expr) { } + protected override void VisitObjectLiteral(Expr.ObjectLiteral expr) { } + protected override void VisitTemplateLiteral(Expr.TemplateLiteral expr) { } + protected override void VisitClassExpr(Expr.ClassExpr expr) { } + protected override void VisitDynamicImport(Expr.DynamicImport expr) { } + + protected override void VisitCall(Expr.Call expr) => Visit(expr.Callee); + protected override void VisitCallPrivate(Expr.CallPrivate expr) => Visit(expr.Object); + protected override void VisitNew(Expr.New expr) => Visit(expr.Callee); + protected override void VisitGetIndex(Expr.GetIndex expr) => Visit(expr.Object); + protected override void VisitTaggedTemplateLiteral(Expr.TaggedTemplateLiteral expr) => Visit(expr.Tag); + + protected override void VisitTernary(Expr.Ternary expr) + { + Visit(expr.Condition); + Visit(expr.ElseBranch); + } + + protected override void VisitArrowFunction(Expr.ArrowFunction expr) + { + if (expr.ExpressionBody is not null) + Visit(expr.ExpressionBody); + } + + protected override void VisitDestructuringAssign(Expr.DestructuringAssign expr) + { + if (expr.RawDefault is not null) + Visit(expr.RawDefault); + } + + protected override void VisitSetIndex(Expr.SetIndex expr) + { + Visit(expr.Object); + Visit(expr.Value); + } + + protected override void VisitCompoundSetIndex(Expr.CompoundSetIndex expr) + { + Visit(expr.Object); + Visit(expr.Value); + } + + protected override void VisitLogicalSetIndex(Expr.LogicalSetIndex expr) + { + Visit(expr.Object); + Visit(expr.Value); + } + } +} diff --git a/src/SharpTS/Parsing/Parser.Statements.cs b/src/SharpTS/Parsing/Parser.Statements.cs index aee4f139c..b63f42c6a 100644 --- a/src/SharpTS/Parsing/Parser.Statements.cs +++ b/src/SharpTS/Parsing/Parser.Statements.cs @@ -199,6 +199,9 @@ private Stmt ForStatement() private Stmt FinishTraditionalFor(Stmt? initializer) { + if (initializer is not null) + new PrivateInForInitializerValidator().Visit(initializer); + Expr? condition = null; if (!Check(TokenType.SEMICOLON)) { diff --git a/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs b/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs index d03d71e62..81a179a3a 100644 --- a/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs +++ b/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs @@ -41,6 +41,7 @@ public static class AstNodeCatalog typeof(Expr.Get), typeof(Expr.Set), typeof(Expr.GetPrivate), + typeof(Expr.PrivateIn), typeof(Expr.SetPrivate), typeof(Expr.CallPrivate), typeof(Expr.This), diff --git a/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs b/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs index 024d5211e..946e979cc 100644 --- a/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs +++ b/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs @@ -41,6 +41,7 @@ public virtual void Visit(Expr expr) case Expr.Get e: VisitGet(e); break; case Expr.Set e: VisitSet(e); break; case Expr.GetPrivate e: VisitGetPrivate(e); break; + case Expr.PrivateIn e: VisitPrivateIn(e); break; case Expr.SetPrivate e: VisitSetPrivate(e); break; case Expr.CallPrivate e: VisitCallPrivate(e); break; case Expr.This e: VisitThis(e); break; @@ -233,6 +234,8 @@ protected virtual void VisitGetPrivate(Expr.GetPrivate expr) Visit(expr.Object); } + protected virtual void VisitPrivateIn(Expr.PrivateIn expr) => Visit(expr.Object); + protected virtual void VisitSetPrivate(Expr.SetPrivate expr) { Visit(expr.Object); diff --git a/src/SharpTS/Runtime/RuntimeEnvironment.cs b/src/SharpTS/Runtime/RuntimeEnvironment.cs index 726574261..46a031583 100644 --- a/src/SharpTS/Runtime/RuntimeEnvironment.cs +++ b/src/SharpTS/Runtime/RuntimeEnvironment.cs @@ -31,6 +31,14 @@ internal SharpTSClass? PrivateClass set => _privateClass = value; } + internal SharpTSClass? FindPrivateClass(string name) + { + for (RuntimeEnvironment? scope = this; scope is not null; scope = scope.Enclosing) + if (scope._privateClass is { } owner && owner.DeclaresPrivateName(name)) + return owner; + return null; + } + public RuntimeEnvironment(RuntimeEnvironment? enclosing = null, bool? strictMode = null) : base(enclosing, strictMode) { diff --git a/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs b/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs index 0f001d98d..33289808a 100644 --- a/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs @@ -39,14 +39,17 @@ public class SharpTSAsyncFunction : ISharpTSAsyncCallable, ITypeCategorized private readonly RuntimeEnvironment _closure; private readonly int _arity; internal SharpTSClass? PrivateOwner { get; set; } + private readonly bool _receiverBound; // JS: functions (including async) are objects and support property assignment. private Dictionary? _properties; - public SharpTSAsyncFunction(Stmt.Function declaration, RuntimeEnvironment closure) + public SharpTSAsyncFunction(Stmt.Function declaration, RuntimeEnvironment closure, bool receiverBound = false) { _declaration = declaration; _closure = closure; - _arity = declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); + _receiverBound = receiverBound; + _arity = declaration.IsPrivate ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); } public bool TryGetProperty(string name, out object? value) @@ -117,7 +120,12 @@ public void SetProperty(string name, object? value) /// public async Task CallAsync(Interpreter interpreter, List arguments) { - RuntimeEnvironment environment = new(_closure) { PrivateClass = PrivateOwner }; + RuntimeEnvironment environment = new(_closure, strictMode: _declaration.IsPrivate ? true : null) { PrivateClass = PrivateOwner }; + if (_declaration.IsPrivate) + { + environment.Define("this", _receiverBound && _closure.TryGet("this", out var receiver) + ? receiver.ToObject() : SharpTSUndefined.Instance); + } await ParameterBinder.BindAsync(_declaration.Parameters, arguments, environment, interpreter); if (_declaration.Body == null) @@ -164,7 +172,7 @@ public SharpTSAsyncFunction Bind(SharpTSInstance instance) // 'super' not in scope - ignore } - return new SharpTSAsyncFunction(_declaration, environment) { PrivateOwner = PrivateOwner }; + return new SharpTSAsyncFunction(_declaration, environment, receiverBound: true) { PrivateOwner = PrivateOwner }; } /// @@ -176,7 +184,7 @@ public SharpTSAsyncFunction BindThisValue(object? thisObject) { RuntimeEnvironment environment = new(_closure); environment.Define("this", thisObject); - return new SharpTSAsyncFunction(_declaration, environment) { PrivateOwner = PrivateOwner }; + return new SharpTSAsyncFunction(_declaration, environment, receiverBound: true) { PrivateOwner = PrivateOwner }; } public SharpTSAsyncFunction BindStatic(SharpTSClass klass) @@ -185,7 +193,7 @@ public SharpTSAsyncFunction BindStatic(SharpTSClass klass) environment.Define("this", klass); if (klass.Superclass != null) environment.Define("super", klass.Superclass); - return new SharpTSAsyncFunction(_declaration, environment) { PrivateOwner = PrivateOwner }; + return new SharpTSAsyncFunction(_declaration, environment, receiverBound: true) { PrivateOwner = PrivateOwner }; } public override string ToString() => $""; diff --git a/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs b/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs index 1a431b90e..aee447740 100644 --- a/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs @@ -39,8 +39,8 @@ public SharpTSAsyncGeneratorFunction(Stmt.Function declaration, RuntimeEnvironme _thisBound = thisBound; _boundThis = boundThis; _boundSuper = boundSuper; - _arity = declaration.Parameters?.Count( - p => p.DefaultValue == null && !p.IsRest && !p.IsOptional) ?? 0; + _arity = declaration.IsPrivate ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters?.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional) ?? 0; } public int Arity() => _arity; @@ -48,7 +48,11 @@ public SharpTSAsyncGeneratorFunction(Stmt.Function declaration, RuntimeEnvironme public object? Call(Interpreter interpreter, List arguments) { // Create a new environment for this generator invocation - RuntimeEnvironment environment = new(_closure) { PrivateClass = PrivateOwner }; + RuntimeEnvironment environment = new(_closure, strictMode: _declaration.IsPrivate ? true : null) { PrivateClass = PrivateOwner }; + if (_declaration.IsPrivate) + { + environment.Define("this", _thisBound ? _boundThis : SharpTSUndefined.Instance); + } // #775: an async generator expression / object method binds its own dynamic `this`. The bound // receiver (or globalThis for a plain call) is defined in the generator's OWN body environment, diff --git a/src/SharpTS/Runtime/Types/SharpTSClass.cs b/src/SharpTS/Runtime/Types/SharpTSClass.cs index f886b67a3..7c7ea36f6 100644 --- a/src/SharpTS/Runtime/Types/SharpTSClass.cs +++ b/src/SharpTS/Runtime/Types/SharpTSClass.cs @@ -1,6 +1,7 @@ using SharpTS.Execution; using SharpTS.Parsing; using SharpTS.Runtime; +using SharpTS.Runtime.Exceptions; using SharpTS.TypeSystem; using System.Collections.Frozen; using System.Runtime.CompilerServices; @@ -43,6 +44,8 @@ public class SharpTSClass( public string Name { get; } = name; public SharpTSClass? Superclass { get; } = superclass; public bool IsAbstract { get; } = isAbstract; + internal RuntimeEnvironment? InitializerEnvironment { get; set; } + internal IReadOnlyList? OrderedInstanceFields { get; set; } private readonly FrozenDictionary _methods = methods.ToFrozenDictionary(); private readonly FrozenDictionary _staticMethods = staticMethods.ToFrozenDictionary(); private readonly Dictionary _staticProperties = staticProperties; @@ -82,6 +85,7 @@ public class SharpTSClass( private readonly List _instancePrivateFields = instancePrivateFields ?? []; private readonly FrozenDictionary _privateMethods = privateMethods?.ToFrozenDictionary() ?? FrozenDictionary.Empty; private readonly Dictionary _staticPrivateFields = staticPrivateFields ?? []; + private readonly HashSet _installedStaticPrivateFields = new(StringComparer.Ordinal); private readonly FrozenDictionary _staticPrivateMethods = staticPrivateMethods?.ToFrozenDictionary() ?? FrozenDictionary.Empty; // Auto-accessor backing storage (TypeScript 4.9+) @@ -104,10 +108,7 @@ public int Arity() // Initialize instance fields with their initializers (before constructor runs) // Superclass fields are initialized first via inheritance chain - InitializeInstanceFields(interpreter, instance); - - // Initialize ES2022 private fields (not inherited, each class has its own storage) - InitializePrivateFields(interpreter, instance); + InitializeOrderedFields(interpreter, instance); // Initialize auto-accessor backing storage (before constructor runs) InitializeAutoAccessors(interpreter, instance); @@ -183,13 +184,13 @@ protected void InitializeInstanceFields(Interpreter interpreter, SharpTSInstance foreach (var field in _instanceFields) { object? value = field.Initializer != null - ? interpreter.Evaluate(field.Initializer) + ? interpreter.EvaluateClassInitializer(this, instance, field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; // Check if this is a computed property name if (field.ComputedKey != null) { - object? key = interpreter.Evaluate(field.ComputedKey); + object? key = interpreter.EvaluateClassInitializer(this, instance, field.ComputedKey); if (key is SharpTSSymbol symbol) { // Symbol key - store in symbol fields @@ -210,6 +211,27 @@ protected void InitializeInstanceFields(Interpreter interpreter, SharpTSInstance } } + private void InitializeOrderedFields(Interpreter interpreter, SharpTSInstance instance) + { + Superclass?.InitializeOrderedFields(interpreter, instance); + var privateFields = new Dictionary(); + _privateFieldStorage.Add(instance, privateFields); + foreach (var field in OrderedInstanceFields ?? _instanceFields.Concat(_instancePrivateFields).ToList()) + { + object? value = field.Initializer != null + ? interpreter.EvaluateClassInitializer(this, instance, field.Initializer) + : field.ComputedKey != null || field.IsPrivate ? SharpTSUndefined.Instance : null; + if (field.IsPrivate) privateFields[field.Name.Lexeme] = value; + else if (field.ComputedKey != null) + { + var key = interpreter.EvaluateClassInitializer(this, instance, field.ComputedKey); + if (key is SharpTSSymbol symbol) instance.SetBySymbol(symbol, value); + else instance.SetRawField(key?.ToString() ?? "undefined", value); + } + else instance.SetRawField(field.Name.Lexeme, value); + } + } + public ISharpTSCallable? FindMethod(string name) { // Check cache first @@ -501,6 +523,16 @@ private void BindPrivateMemberOwner(ISharpTSCallable method) internal bool HasPrivateBrand(object receiver) => _privateFieldStorage.TryGetValue(receiver, out _); + internal bool DeclaresPrivateName(string name) + => _instancePrivateFields.Any(field => field.Name.Lexeme == name) + || _privateMethods.ContainsKey(name) || _staticPrivateFields.ContainsKey(name) + || _staticPrivateMethods.ContainsKey(name); + + internal bool HasInstalledPrivateField(object receiver, string name) + => _privateFieldStorage.TryGetValue(receiver, out var fields) && fields.ContainsKey(name); + + internal bool HasInstalledStaticPrivateField(string name) => _installedStaticPrivateFields.Contains(name); + /// /// Initializes private fields for an instance. Each class in the hierarchy /// has its own private field storage - private fields are NOT inherited. @@ -512,16 +544,15 @@ protected void InitializePrivateFields(Interpreter interpreter, SharpTSInstance Superclass?.InitializePrivateFields(interpreter, instance); var fields = new Dictionary(); + _privateFieldStorage.Add(instance, fields); foreach (var field in _instancePrivateFields) { object? value = field.Initializer != null - ? interpreter.Evaluate(field.Initializer) + ? interpreter.EvaluateClassInitializer(this, instance, field.Initializer) : null; fields[field.Name.Lexeme] = value; } - // Add to ConditionalWeakTable - GC-friendly storage - _privateFieldStorage.Add(instance, fields); } /// @@ -581,8 +612,16 @@ public void SetPrivateField(object instance, string name, object? value) /// Sets a static private field value. /// public void SetStaticPrivateField(string name, object? value) + { + if (!_installedStaticPrivateFields.Contains(name)) + throw new ThrowException(new SharpTSTypeError($"Cannot access private member {name} before initialization")); + _staticPrivateFields[name] = value; + } + + internal void InstallStaticPrivateField(string name, object? value) { _staticPrivateFields[name] = value; + _installedStaticPrivateFields.Add(name); } /// @@ -622,7 +661,7 @@ protected void InitializeAutoAccessors(Interpreter interpreter, SharpTSInstance foreach (var autoAccessor in _instanceAutoAccessors) { object? value = autoAccessor.Initializer != null - ? interpreter.Evaluate(autoAccessor.Initializer) + ? interpreter.EvaluateClassInitializer(this, instance, autoAccessor.Initializer) : null; storage[autoAccessor.Name.Lexeme] = value; } diff --git a/src/SharpTS/Runtime/Types/SharpTSFunction.cs b/src/SharpTS/Runtime/Types/SharpTSFunction.cs index a2f5ea531..011855c47 100644 --- a/src/SharpTS/Runtime/Types/SharpTSFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSFunction.cs @@ -59,6 +59,7 @@ public class SharpTSFunction : ISharpTSCallable, ITypeCategorized private readonly Stmt.Function _declaration; private readonly RuntimeEnvironment _closure; private readonly int _arity; + internal bool IsPrivateMethod => _declaration.IsPrivate; internal SharpTSClass? PrivateOwner { get; set; } // `this` value stored on the function itself (from BindThis) rather than in an // extra closure scope — otherwise the resolver's scope-distance count wouldn't @@ -81,7 +82,9 @@ private SharpTSFunction(Stmt.Function declaration, RuntimeEnvironment closure, o _closure = closure; _boundThis = boundThis; _hasBoundThis = hasBoundThis; - _arity = declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); + _arity = declaration.IsPrivate + ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); InitializeIntrinsicProperties(declaration.Name.Lexeme); } @@ -217,7 +220,7 @@ public bool TryGetAccessor(string name, out ISharpTSCallable? getter, out ISharp public int Arity() => _arity; internal bool IsStrict - => _closure.IsStrictMode + => _declaration.IsPrivate || _closure.IsStrictMode || (_declaration.Body is not null && Parsing.DirectivePrologue.HasUseStrict(_declaration.Body)); diff --git a/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs b/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs index 6774754e5..8c9b0c5da 100644 --- a/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs @@ -42,7 +42,8 @@ public SharpTSGeneratorFunction(Stmt.Function declaration, RuntimeEnvironment cl _thisBound = thisBound; _boundThis = boundThis; _boundSuper = boundSuper; - _arity = declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); + _arity = declaration.IsPrivate ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); } public int Arity() => _arity; @@ -53,7 +54,11 @@ public SharpTSGeneratorFunction(Stmt.Function declaration, RuntimeEnvironment cl public object? Call(Interpreter interpreter, List arguments) { // Create environment and bind parameters (like a regular function) - RuntimeEnvironment environment = new(_closure) { PrivateClass = PrivateOwner }; + RuntimeEnvironment environment = new(_closure, strictMode: _declaration.IsPrivate ? true : null) { PrivateClass = PrivateOwner }; + if (_declaration.IsPrivate) + { + environment.Define("this", _thisBound ? _boundThis : SharpTSUndefined.Instance); + } // #775: a generator expression / object generator method binds its own dynamic `this`. The bound // receiver (or globalThis for a plain call) is defined in the generator's OWN body environment — // NOT a new parent scope inserted above the closure — so a captured enclosing-function local keeps diff --git a/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs b/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs index 959b1bbe0..0534fc9ca 100644 --- a/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs +++ b/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs @@ -274,6 +274,9 @@ private static void CollectAssignedPathsFromExpr(Expr expr, HashSet sources.Any(s => SignatureRelatedTo(s, t))); + } + + private List ProjectGenericConstructors(TypeInfo.GenericClass type) + { + var arguments = type.TypeParams.Cast().ToList(); + var members = CollectGenericClassMembers(type, arguments, includeNonPublic: true); + foreach (var (name, member) in CollectConstructorStaticMembers(type)) + members.Add("'static:" + name, member); + if (type.StringIndexType is { } stringIndex) members.Add("'stringIndex", stringIndex); + if (type.NumberIndexType is { } numberIndex) members.Add("'numberIndex", numberIndex); + if (type.SymbolIndexType is { } symbolIndex) members.Add("'symbolIndex", symbolIndex); + var result = new TypeInfo.Record(members.ToFrozenDictionary()); + + var constructor = ResolveClassMemberTypeSubstituted( + new TypeInfo.InstantiatedGeneric(type, arguments), "constructor"); + var functions = constructor switch + { + TypeInfo.Function f => new List { f }, + TypeInfo.OverloadedFunction overloads => overloads.Signatures, + _ => [new TypeInfo.Function([], TypeInfo.Void.Shared)] + }; + return functions.Select(f => new NormalizedSignature(type.TypeParams, + new TypeInfo.Function(f.ParamTypes, result, f.RequiredParams, f.HasRestParam, + f.ThisType, f.ParamNames))).ToList(); + } + + private Dictionary CollectConstructorStaticMembers(TypeInfo type) + { + Dictionary members = []; + Dictionary substitutions = []; + TypeInfo? current = type; + for (int guard = 0; current is not null && guard < 256; guard++) + { + ClassMetadataCore? core; + if (current is TypeInfo.InstantiatedGeneric { GenericDefinition: TypeInfo.GenericClass generic } instance) + { + var arguments = instance.TypeArguments.Select(a => Substitute(a, substitutions)).ToList(); + substitutions = GenericClassSubs(generic, arguments); + core = generic.Core; + } + else + core = current switch + { + TypeInfo.Class c => c.Core, + TypeInfo.GenericClass c => c.Core, + TypeInfo.MutableClass c => c.Freeze().Core, + _ => null + }; + if (core is null) break; + foreach (var (name, member) in core.StaticProperties.Concat(core.StaticMethods)) + members.TryAdd(name, SubstitutePreservingSignatures(member, substitutions)); + current = core.Superclass; + } + return members; + } + + private static bool ConstructorPrivateBrandsRelated(TypeInfo target, TypeInfo source, bool isStatic) + { + var sourceCores = EnumerateClassCores(source).ToList(); + foreach (var core in EnumerateClassCores(target)) + { + bool branded = isStatic + ? core.StaticPrivateFieldTypes.Count > 0 || core.StaticPrivateMethodTypes.Count > 0 + : core.PrivateFieldTypes.Count > 0 || core.PrivateMethodTypes.Count > 0; + if (branded && !sourceCores.Any(s => ReferenceEquals(s, core) + || core.DeclarationId != 0 && s.DeclarationId == core.DeclarationId)) + return false; + } + return true; + } + + private static bool ConstructorStaticAccessibilityRelated(TypeInfo target, TypeInfo source) + { + var targets = CollectStaticBrands(target); + var sources = CollectStaticBrands(source); + foreach (var (name, targetBrand) in targets) + { + if (!sources.TryGetValue(name, out var sourceBrand)) continue; + if (targetBrand.Access == AccessModifier.Public && sourceBrand.Access == AccessModifier.Public) continue; + if (targetBrand.Access != sourceBrand.Access) return false; + if (targetBrand.DeclaringClassId != 0 && targetBrand.DeclaringClassId == sourceBrand.DeclaringClassId) continue; + if (targetBrand.Access == AccessModifier.Protected + && SourceDerivesFromDeclaration(source, targetBrand.DeclaringClassId)) continue; + return false; + } + return true; + } + + private static Dictionary CollectStaticBrands(TypeInfo type) + { + Dictionary brands = []; + foreach (var core in EnumerateClassCores(type)) + { + foreach (var name in core.StaticProperties.Keys) + brands.TryAdd(name, new MemberAccessBrand( + core.StaticFieldAccessMap.GetValueOrDefault(name, AccessModifier.Public), core.DeclarationId)); + foreach (var name in core.StaticMethods.Keys) + brands.TryAdd(name, new MemberAccessBrand( + core.StaticMethodAccessMap.GetValueOrDefault(name, AccessModifier.Public), core.DeclarationId)); + } + return brands; + } +} diff --git a/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs b/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs index 7ff037159..43af4d011 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs @@ -442,6 +442,9 @@ actual is TypeInfo.GenericClass actualConstructor && if (!_strictNullChecks && actual is TypeInfo.Null or TypeInfo.Undefined) return expected is not TypeInfo.Never; + if (expected is TypeInfo.GenericClass targetClass && actual is TypeInfo.GenericClass sourceClass) + return GenericConstructorsRelated(targetClass, sourceClass); + // Namespace/module types (typeof someNamespace / typeof import(...)) have no dedicated // relation anywhere below, so two structurally-identical namespaces — even the exact same // one referenced twice, e.g. `true ? af : null` vs `true ? null : af` — fell through to the diff --git a/src/SharpTS/TypeSystem/TypeChecker.Dispatch.cs b/src/SharpTS/TypeSystem/TypeChecker.Dispatch.cs index fa8bb5141..25f2888be 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Dispatch.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Dispatch.cs @@ -34,6 +34,7 @@ public partial class TypeChecker Expr.Get e => VisitGet(e), Expr.Set e => VisitSet(e), Expr.GetPrivate e => VisitGetPrivate(e), + Expr.PrivateIn e => VisitPrivateIn(e), Expr.SetPrivate e => VisitSetPrivate(e), Expr.CallPrivate e => VisitCallPrivate(e), Expr.This e => VisitThis(e), diff --git a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs index 90f01864b..a0276dc78 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs @@ -2574,7 +2574,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) { var method = implementations[0]; var funcType = BuildMethodFuncType(method); - if (method.IsStatic) + if (method.IsPrivate) + { + if (method.IsStatic) mutableClass.StaticPrivateMethods[methodName] = funcType; + else mutableClass.PrivateMethods[methodName] = funcType; + } + else if (method.IsStatic) mutableClass.StaticMethods[methodName] = funcType; else mutableClass.Methods[methodName] = funcType; @@ -2593,7 +2598,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? TypeInfo.Any.Shared; - if (field.IsStatic) + if (field.IsPrivate) + { + if (field.IsStatic) mutableClass.StaticPrivateFields[fieldName] = fieldType; + else mutableClass.PrivateFields[fieldName] = fieldType; + } + else if (field.IsStatic) mutableClass.StaticProperties[fieldName] = fieldType; else mutableClass.FieldTypes[fieldName] = fieldType; @@ -2694,6 +2704,8 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Check method bodies TypeEnvironment classEnv = new(_environment); + if (classExpr.Name is { } innerName) + classEnv.Define(innerName.Lexeme, classExprResultType); if (classTypeParams != null) { for (int i = 0; i < classTypeParams.Count; i++) @@ -2715,6 +2727,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeEnvironment prevEnv = _environment; TypeInfo.Class? prevClass = _currentClass; _environment = classEnv; + _privateInEnclosingClasses.Push(prevClass); _currentClass = classTypeForBody is TypeInfo.Class c ? c : mutableClass.Freeze(); // Set when a method's inferred (un-annotated) return type is resolved during the body @@ -2729,7 +2742,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) { TypeEnvironment methodEnv; if (method.IsStatic) + { methodEnv = new TypeEnvironment(prevEnv); + if (classExpr.Name is { } staticInnerName) + methodEnv.Define(staticInnerName.Lexeme, classExprResultType); + } else methodEnv = new TypeEnvironment(_environment); @@ -2754,7 +2771,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) } else { - declaredMethodType = method.IsStatic + declaredMethodType = method.IsPrivate + ? method.IsStatic + ? classTypeForBody.StaticPrivateMethodTypes[method.Name.Lexeme] + : classTypeForBody.PrivateMethodTypes[method.Name.Lexeme] + : method.IsStatic ? classTypeForBody.StaticMethods[method.Name.Lexeme] : classTypeForBody.Methods[method.Name.Lexeme]; } @@ -2959,7 +2980,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) foreach (var field in classExpr.Fields.Where(f => f.Initializer != null)) { TypeInfo initType = CheckExpr(field.Initializer!); - TypeInfo fieldDeclaredType = field.IsStatic + TypeInfo fieldDeclaredType = field.IsPrivate + ? field.IsStatic + ? classTypeForBody.StaticPrivateFieldTypes[field.Name.Lexeme] + : classTypeForBody.PrivateFieldTypes[field.Name.Lexeme] + : field.IsStatic ? classTypeForBody.StaticProperties[field.Name.Lexeme] : classTypeForBody.FieldTypes[field.Name.Lexeme]; @@ -2970,6 +2995,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) finally { _environment = prevEnv; + _privateInEnclosingClasses.Pop(); _currentClass = prevClass; } diff --git a/src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs b/src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs new file mode 100644 index 000000000..828b542b4 --- /dev/null +++ b/src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs @@ -0,0 +1,39 @@ +using SharpTS.Parsing; +using SharpTS.TypeSystem.Exceptions; + +namespace SharpTS.TypeSystem; + +public partial class TypeChecker +{ + private readonly Stack _privateInEnclosingClasses = []; + + internal TypeInfo VisitPrivateIn(Expr.PrivateIn expr) + { + if (_currentClass == null) + throw new TypeCheckException("Private identifiers are not allowed outside class bodies.", tsCode: "TS18016"); + string name = expr.Name.Lexeme; + var owner = new[] { _currentClass }.Concat(_privateInEnclosingClasses).FirstOrDefault(candidate => candidate is not null + && (candidate.PrivateFieldTypes.ContainsKey(name) || candidate.StaticPrivateFieldTypes.ContainsKey(name) + || candidate.PrivateMethodTypes.ContainsKey(name) || candidate.StaticPrivateMethodTypes.ContainsKey(name))); + if (owner is null) + throw new TypeCheckException($"Private name '{name}' does not exist on class '{_currentClass.Name}'.", tsCode: "TS2339"); + _typeMap.SetPrivateInOwner(expr.Name, owner); + var type = CheckExpr(expr.Object); + if (type is TypeInfo.Unknown) + throw new TypeCheckException("Object is of type 'unknown'.", tsCode: "TS18046"); + if (!IsAnyPermissive(type) && IsInvalidPrivateInReceiver(type)) + throw new TypeCheckException($"Type '{type}' is not assignable to type 'object'.", tsCode: "TS2322"); + return TypeInfo.Primitive.Boolean; + } + + private bool IsInvalidPrivateInReceiver(TypeInfo type) => type switch + { + TypeInfo.Primitive or TypeInfo.String or TypeInfo.NumberLiteral or TypeInfo.StringLiteral + or TypeInfo.BooleanLiteral or TypeInfo.Null or TypeInfo.Undefined + or TypeInfo.BigInt or TypeInfo.Symbol or TypeInfo.UniqueSymbol or TypeInfo.Void or TypeInfo.Unknown => true, + TypeInfo.TypeParameter parameter => ApparentTypeOf(parameter) is not { } constraint + || constraint is TypeInfo.Any || IsInvalidPrivateInReceiver(constraint), + TypeInfo.Union union => union.Types.Any(IsInvalidPrivateInReceiver), + _ => false, + }; +} diff --git a/src/SharpTS/TypeSystem/TypeChecker.Properties.cs b/src/SharpTS/TypeSystem/TypeChecker.Properties.cs index 26602996f..97535ef43 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Properties.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Properties.cs @@ -1300,6 +1300,10 @@ private TypeInfo CheckGetPrivate(Expr.GetPrivate get) { return fieldType; } + if (_currentClass.PrivateMethodTypes.TryGetValue(fieldName, out var methodType)) + return methodType; + if (_currentClass.StaticPrivateMethodTypes.TryGetValue(fieldName, out var staticMethodType)) + return staticMethodType; // Check static private fields if accessing on the class itself if (IsStaticPrivateConstructor(objType) && _currentClass.StaticPrivateFieldTypes.TryGetValue(fieldName, out var staticFieldType)) diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs index f4d0803a2..55944125a 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs @@ -89,7 +89,7 @@ private TypeInfo.Class PublishInitialClassType( _environment.DefineType(classStmt.Name.Lexeme, genericClassType); // For body check, freeze the mutable class (methods/fields have TypeParameter types) classTypeForBody = mutableClass.Freeze(); - _typeMap.SetClassType(classStmt.Name.Lexeme, classTypeForBody); + _typeMap.SetClassType(classStmt, classTypeForBody); } else { @@ -97,7 +97,7 @@ private TypeInfo.Class PublishInitialClassType( TypeInfo.Class classType = mutableClass.Freeze(); _environment.Define(classStmt.Name.Lexeme, classType); _environment.DefineType(classStmt.Name.Lexeme, classType); - _typeMap.SetClassType(classStmt.Name.Lexeme, classType); + _typeMap.SetClassType(classStmt, classType); classTypeForBody = classType; } return classTypeForBody; @@ -282,34 +282,44 @@ private bool CheckClassStaticFieldInitializers( TypeInfo.MutableClass mutableClass) { bool anyInferredFieldTypeResolved = false; - - // Second pass: check static property initializers at class scope - foreach (var field in classStmt.Fields) + var previousClass = _currentClass; + _privateInEnclosingClasses.Push(previousClass); + _currentClass = classTypeForBody; + try { - if (field.IsStatic && field.Initializer != null) + + // Static private names are visible before their fields are installed. + foreach (var field in classStmt.Fields) { - TypeInfo initType = CheckExpr(field.Initializer); - // For ES2022 private static fields, look in StaticPrivateFieldTypes - TypeInfo staticFieldDeclaredType = field.IsPrivate - ? classTypeForBody.StaticPrivateFieldTypes[GetFieldMemberName(field)] - : classTypeForBody.StaticProperties[GetFieldMemberName(field)]; - if (field.TypeAnnotation is null) - { - TypeInfo inferredFieldType = WidenLiteralType(initType); - if (field.IsPrivate) - mutableClass.StaticPrivateFields[GetFieldMemberName(field)] = inferredFieldType; - else - mutableClass.StaticProperties[GetFieldMemberName(field)] = inferredFieldType; - anyInferredFieldTypeResolved = true; - continue; - } - if (!IsCompatible(staticFieldDeclaredType, initType)) + if (field.IsStatic && field.Initializer != null) { - throw new TypeCheckException($" Cannot assign type '{initType}' to static property '{field.Name.Lexeme}' of type '{staticFieldDeclaredType}'.", tsCode: "TS2322"); + TypeInfo initType = CheckExpr(field.Initializer); + TypeInfo staticFieldDeclaredType = field.IsPrivate + ? classTypeForBody.StaticPrivateFieldTypes[GetFieldMemberName(field)] + : classTypeForBody.StaticProperties[GetFieldMemberName(field)]; + if (field.TypeAnnotation is null) + { + TypeInfo inferredFieldType = WidenLiteralType(initType); + if (field.IsPrivate) + mutableClass.StaticPrivateFields[GetFieldMemberName(field)] = inferredFieldType; + else + mutableClass.StaticProperties[GetFieldMemberName(field)] = inferredFieldType; + anyInferredFieldTypeResolved = true; + continue; + } + if (!IsCompatible(staticFieldDeclaredType, initType)) + { + throw new TypeCheckException($" Cannot assign type '{initType}' to static property '{field.Name.Lexeme}' of type '{staticFieldDeclaredType}'.", tsCode: "TS2322"); + } } } + return anyInferredFieldTypeResolved; + } + finally + { + _privateInEnclosingClasses.Pop(); + _currentClass = previousClass; } - return anyInferredFieldTypeResolved; } // Checks initializers at declaration scope before static blocks, preserving diagnostic order. @@ -392,14 +402,14 @@ private void RepublishInferredClassType( var frozen = mutableClass.FreezeGeneric(classTypeParams); _environment.Define(classStmt.Name.Lexeme, frozen); _environment.DefineType(classStmt.Name.Lexeme, frozen); - _typeMap.SetClassType(classStmt.Name.Lexeme, mutableClass.Freeze()); + _typeMap.SetClassType(classStmt, mutableClass.Freeze()); } else { var refrozen = mutableClass.Freeze(); _environment.Define(classStmt.Name.Lexeme, refrozen); _environment.DefineType(classStmt.Name.Lexeme, refrozen); - _typeMap.SetClassType(classStmt.Name.Lexeme, refrozen); + _typeMap.SetClassType(classStmt, refrozen); } // Structural compatibility results cache on CacheKey() (carries the stable DeclarationId), // so any comparison made against the placeholder during the body pass must not be reused. diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs index 5790b24e5..5f4af520e 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs @@ -537,6 +537,7 @@ private void CheckClassStaticBlocks(Stmt.Class classStmt, TypeInfo.Class classTy var previousClass = _currentClass; _inStaticBlock = true; _inStaticMethod = true; + _privateInEnclosingClasses.Push(previousClass); _currentClass = classTypeForBody; try @@ -550,6 +551,7 @@ private void CheckClassStaticBlocks(Stmt.Class classStmt, TypeInfo.Class classTy { _inStaticBlock = previousInStaticBlock; _inStaticMethod = previousInStaticMethod; + _privateInEnclosingClasses.Pop(); _currentClass = previousClass; } } @@ -637,6 +639,7 @@ TypeInfo.MutableClass preregisteredClass TypeInfo.Class? prevClass = _currentClass; _environment = classEnv; + _privateInEnclosingClasses.Push(prevClass); _currentClass = classTypeForBody; // Set when a method's inferred (un-annotated) return type is resolved during the body @@ -655,6 +658,7 @@ TypeInfo.MutableClass preregisteredClass finally { _environment = prevEnv; + _privateInEnclosingClasses.Pop(); _currentClass = prevClass; } @@ -1460,7 +1464,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) parentEnv.Define(classStmt.Name.Lexeme, classType); parentEnv.DefineType(classStmt.Name.Lexeme, classType); } - _typeMap.SetClassType(classStmt.Name.Lexeme, classType); + _typeMap.SetClassType(classStmt, classType); } // End EnvironmentScope } diff --git a/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs b/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs index 019417887..ae0e16174 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs @@ -206,6 +206,9 @@ private HashSet AnalyzeInitializationExpression( case Expr.Grouping grouping: assigned = AnalyzeInitializationExpression(grouping.Expression, assigned); break; + case Expr.PrivateIn presence: + assigned = AnalyzeInitializationExpression(presence.Object, assigned); + break; case Expr.Ternary ternary: { var afterCondition = AnalyzeInitializationExpression( diff --git a/src/SharpTS/TypeSystem/TypeChecker.cs b/src/SharpTS/TypeSystem/TypeChecker.cs index 251f7e969..c28d414db 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.cs @@ -3055,6 +3055,7 @@ private TypeInfo GetDeclaredType(Stmt decl) Expr.ImportMeta im => im.Keyword.Line, Expr.Set s => s.Name.Line, Expr.Get g => g.Name.Line, + Expr.PrivateIn presence => presence.Name.Line, Expr.Assign a => a.Name.Line, Expr.Binary b => b.Operator.Line, Expr.Logical l => l.Operator.Line, diff --git a/src/SharpTS/TypeSystem/TypeMap.cs b/src/SharpTS/TypeSystem/TypeMap.cs index cb7a82e2a..ff9482fb9 100644 --- a/src/SharpTS/TypeSystem/TypeMap.cs +++ b/src/SharpTS/TypeSystem/TypeMap.cs @@ -14,6 +14,10 @@ namespace SharpTS.TypeSystem; public class TypeMap { private readonly Dictionary _types = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _privateInOwners = new(ReferenceEqualityComparer.Instance); + + internal void SetPrivateInOwner(Token name, TypeInfo.Class owner) => _privateInOwners[name] = owner; + internal TypeInfo.Class? GetPrivateInOwner(Token name) => _privateInOwners.GetValueOrDefault(name); private readonly Dictionary _classTypes = new(StringComparer.Ordinal); private readonly Dictionary _functionTypes = new(StringComparer.Ordinal); private readonly Dictionary _classExprTypes = new(ReferenceEqualityComparer.Instance); @@ -56,6 +60,16 @@ public class TypeMap /// public void SetClassType(string className, TypeInfo.Class classType) => _classTypes[className] = classType; + private readonly Dictionary _classDeclarations = new(ReferenceEqualityComparer.Instance); + + public void SetClassType(Stmt.Class declaration, TypeInfo.Class classType) + { + _classDeclarations[declaration] = classType; + SetClassType(declaration.Name.Lexeme, classType); + } + + public TypeInfo.Class? GetClassType(Stmt.Class declaration) => _classDeclarations.GetValueOrDefault(declaration); + /// /// Gets the class type by name, or null if not found. /// diff --git a/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs b/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs index 11e2d343a..3c66bbd30 100644 --- a/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs +++ b/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs @@ -91,6 +91,7 @@ public class EmitterSyncTests "get_Resolver", // Abstract: provides IVariableResolver "GetThisField", // Abstract: provides hoisted 'this' field "GetHoistedVariableField", // Hoisted variable fields on state machine + "GetFunctionDCField", // #1965: shared arrow emission loads captured-and-mutated locals from the async state machine's function display "ResolveCaptureSourceName", // #1517: typed Promise delegates honor block-shadow capture pivots "SharpTS.Compilation.Emitters.IEmitterContext.get_IL", // IEmitterContext impl "SharpTS.Compilation.Emitters.IEmitterContext.SetStackUnknown", // IEmitterContext impl @@ -194,6 +195,7 @@ public class EmitterSyncTests "get_Resolver", "GetThisField", "GetHoistedVariableField", + "ResolveCaptureSourceName", // #1965: source captured block shadows from their renamed async-generator storage // --- Genuinely different behavior --- // (break/continue/loop-nav/EmitBranchToLabel are inherited from StateMachineExitRoutingEmitter.) "EmitReturn", // Async generator return: store in CurrentField + state -2 diff --git a/tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs b/tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs new file mode 100644 index 000000000..38ed4e192 --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using SharpTS.Compilation; +using SharpTS.Parsing; +using SharpTS.TypeSystem; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public sealed class ClassRuntimeFeatureGatingTests +{ + [Theory] + [InlineData("console.log(1);", false)] + [InlineData("class Item {}", true)] + [InlineData("const Item = class {};", true)] + public void ReceiverAndPrototypeHelpersFollowUserClassSelection(string source, bool selected) + { + var statements = new Parser(new Lexer(source).ScanTokens()).ParseOrThrow(); + var typeMap = new TypeChecker().Check(statements); + Assert.Equal(selected, new RuntimeFeatureDetector().Detect(statements, typeMap).UsesUserClasses); + var compiler = new ILCompiler($"class_gate_{Guid.NewGuid():N}"); + compiler.Compile(statements, typeMap, new DeadCodeAnalyzer(typeMap).Analyze(statements)); + var assembly = Assembly.Load(compiler.SaveToBytes()); + var definition = assembly.GetType("$ClassDefinition", throwOnError: true)!; + Assert.Equal(selected, assembly.GetType("$ClassCapture") is not null); + foreach (string name in new[] { "Create", "ReadCapture", "WriteCapture", "FindEnvironment", "ValidateParent", "InitializeReceiver", "GetParent", "ReadArgument", "ReadPrototypeProperty", "ReadSuper" }) + Assert.Equal(selected, definition.GetMethod(name) is not null); + } +} diff --git a/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs b/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs new file mode 100644 index 000000000..68fa6c098 --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Compilation; +using SharpTS.Parsing; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public sealed class EmittedClassDefinitionRuntimeTests +{ + [Fact] + public void RequiredHandlesCannotBeReadOrCompletedBeforeDeclaration() + { + var owner = new EmittedRuntime().ClassDefinitions; + Assert.Throws(() => owner.Create); + Assert.Throws(owner.CompleteEmission); + Assert.False(owner.IsComplete); + Assert.NotSame(owner, new EmittedRuntime().ClassDefinitions); + Assert.Null(typeof(EmittedRuntime).GetProperty(nameof(EmittedRuntime.ClassDefinitions))!.SetMethod); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReusedEmitterFinalizesFreshDefinitionOwnersAndVerifiableBodies(bool hosted) + { + var emitter = new RuntimeEmitter(TypeProvider.Runtime, emitHosted: hosted); + var owners = new HashSet(); + foreach (string source in new[] { "const n = 1;", "class Item {}", "async function f() { await Promise.resolve(1); } f();", "const Item = class {};" }) + { + var assembly = new PersistedAssemblyBuilder(new AssemblyName($"definitions_{Guid.NewGuid():N}"), typeof(object).Assembly); + var module = assembly.DefineDynamicModule("main"); + var features = new RuntimeFeatureDetector().Detect(new Parser(new Lexer(source).ScanTokens()).ParseOrThrow()); + var runtime = emitter.EmitAll(module, features); + var owner = runtime.ClassDefinitions; + Assert.True(owner.IsComplete); + Assert.True(owners.Add(owner)); + Assert.Same(module, owner.Construct.Module); + Assert.Same(owner.Type, owner.Construct.DeclaringType); + Assert.Equal(features.UsesUserClasses, owner.HasRuntimeParentHelpers); + if (features.UsesUserClasses) + { + Assert.Same(module, owner.Create.Module); + Assert.Same(owner.Type, owner.Create.DeclaringType); + } + else Assert.Throws(() => owner.Create); + Assert.Throws(owner.CompleteEmission); + using var bytes = new MemoryStream(); + assembly.Save(bytes); + bytes.Position = 0; + using var verifier = new ILVerifier(extraProbeDirectories: [AppContext.BaseDirectory]); + Assert.Empty(verifier.Verify(bytes)); + var loaded = Assembly.Load(bytes.ToArray()); + Assert.DoesNotContain(loaded.GetReferencedAssemblies(), reference => reference.Name == "SharpTS"); + Assert.Equal(hosted, loaded.GetReferencedAssemblies().Any(reference => reference.Name == "SharpTS.Hosting.Abstractions")); + Assert.NotNull(loaded.GetType("$ClassDefinition")); + } + } +} diff --git a/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs b/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs index 3ea1582f4..17eea6574 100644 --- a/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs @@ -8,6 +8,41 @@ namespace SharpTS.Tests.CompilerTests; public sealed class PrivateClassElementRegistryTests { + [Fact] + public void MethodValueRejectsForeignCacheAndRequiresAllBodies() + { + var module = new PersistedAssemblyBuilder(new AssemblyName(Guid.NewGuid().ToString("N")), typeof(object).Assembly).DefineDynamicModule("Main"); + var owner = module.DefineType("Box", TypeAttributes.Public); + var adapter = module.DefineType("Value", TypeAttributes.Public); + var foreign = module.DefineType("Foreign", TypeAttributes.Public); + var original = owner.DefineMethod("read", MethodAttributes.Public, typeof(object), Type.EmptyTypes); + var storage = owner.DefineField("brand", typeof(object), FieldAttributes.Static); + var registry = new PrivateClassElementRegistry(); + registry.Declare("Box", owner, storage, [], new Dictionary(), + new Dictionary { ["read"] = original }, new Dictionary()); + var source = new SharpTS.Parsing.Stmt.Function(new SharpTS.Parsing.Token(SharpTS.Parsing.TokenType.IDENTIFIER, "#read", null, 0), + null, null, [], [], null, IsPrivate: true); + var method = adapter.DefineMethod("Invoke", MethodAttributes.Public, typeof(object), [typeof(object)]); + var constructor = adapter.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var initializer = adapter.DefineTypeInitializer(); + var cache = adapter.DefineField("Value", typeof(object), FieldAttributes.Static); + var value = new PrivateMethodValue(adapter, constructor, method, cache, initializer, source); + Assert.Throws(() => registry.DeclareMethodValue("Box", "read", + value with { Cache = foreign.DefineField("Value", typeof(object), FieldAttributes.Static) })); + registry.DeclareMethodValue("Box", "read", value); + Assert.Throws(() => ((IDictionary)registry.Require("Box").MethodValues).Clear()); + Assert.Throws(() => registry.MarkBodiesEmitted("Box")); + original.GetILGenerator().Emit(OpCodes.Ldnull); original.GetILGenerator().Emit(OpCodes.Ret); + method.GetILGenerator().Emit(OpCodes.Ldnull); method.GetILGenerator().Emit(OpCodes.Ret); + constructor.GetILGenerator().Emit(OpCodes.Ret); + Assert.Throws(() => registry.MarkBodiesEmitted("Box")); + initializer.GetILGenerator().Emit(OpCodes.Ret); + registry.MarkBodiesEmitted("Box"); + Assert.Throws(() => registry.DeclareMethodValue("Box", "read", value)); + registry.CompleteEmission(); + Assert.Throws(() => registry.DeclareMethodValue("Box", "read", value)); + } + [Fact] public void GenericBridgeRejectsForeignOwnerAndRequiresItsStorageBody() { diff --git a/tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs b/tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs new file mode 100644 index 000000000..9612b9b85 --- /dev/null +++ b/tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs @@ -0,0 +1,252 @@ +using SharpTS.Parsing; +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.ParserTests; + +public sealed class PrivateInParserTests +{ + private static Expr ParseExpression(string source) + => Assert.IsType(Assert.Single(TestHarness.ParseOrThrow(source))).Expr; + + [Fact] + public void BarePrivateNameFormsAPresenceExpression() + { + var expression = Assert.IsType(ParseExpression("#value in object;")); + Assert.Equal("#value", expression.Name.Lexeme); + Assert.Equal("object", Assert.IsType(expression.Object).Name.Lexeme); + } + + [Fact] + public void PresenceRightOperandIncludesShiftAndArithmeticExpressions() + { + var expression = Assert.IsType(ParseExpression("#value in object + other << 1;")); + var shift = Assert.IsType(expression.Object); + Assert.Equal(TokenType.LESS_LESS, shift.Operator.Type); + Assert.Equal(TokenType.PLUS, Assert.IsType(shift.Left).Operator.Type); + } + + [Fact] + public void PresenceBindsBeforeEqualityAndLogicalOperators() + { + var logical = Assert.IsType(ParseExpression("#value in object === true && enabled;")); + Assert.Equal(TokenType.AND_AND, logical.Operator.Type); + var equality = Assert.IsType(logical.Left); + Assert.Equal(TokenType.EQUAL_EQUAL_EQUAL, equality.Operator.Type); + Assert.IsType(equality.Left); + } + + [Theory] + [InlineData("#value in object in other;", TokenType.IN)] + [InlineData("#value in object < other;", TokenType.LESS)] + [InlineData("#value in object instanceof Other;", TokenType.INSTANCEOF)] + public void FollowingRelationalOperatorsAssociateToTheLeft(string source, TokenType operation) + { + var expression = Assert.IsType(ParseExpression(source)); + Assert.Equal(operation, expression.Operator.Type); + Assert.IsType(expression.Left); + } + + [Fact] + public void PrivateNameAndInMayBeSeparatedByALineTerminator() + => Assert.IsType(ParseExpression("#value\nin object;")); + + [Theory] + [InlineData("#value;")] + [InlineData("(#value) in object;")] + [InlineData("!#value in object;")] + [InlineData("delete #value in object;")] + [InlineData("#value + 1;")] + [InlineData("#value === object;")] + [InlineData("object in #value;")] + [InlineData("object < #value in object;")] + [InlineData("#value in #other in object;")] + [InlineData("#value in;")] + [InlineData("#value in object = true;")] + [InlineData("(#value in object) = true;")] + [InlineData("#value in object += true;")] + public void PrivateNamesAreRejectedOutsideTheBarePresenceProduction(string expression) + { + var source = $$""" + class Owner { + #value: number = 1; + #other: number = 2; + check(object: any): void { {{expression}} } + } + """; + var result = new Parser(new Lexer(source).ScanTokens()).Parse(); + Assert.False(result.IsSuccess); + Assert.NotEmpty(result.Diagnostics); + } + + [Theory] + [InlineData("for (#value in object) {}")] + [InlineData("for (let #value in object) {}")] + public void PrivatePresenceCannotBeAForInBinding(string statement) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: any): void { {{statement}} } + } + """; + Assert.False(new Parser(new Lexer(source).ScanTokens()).Parse().IsSuccess); + } + + [Fact] + public void ParenthesizedPresenceCanInitializeATraditionalForLoop() + { + var source = """ + class Owner { + #value: number = 1; + check(object: any): void { + for (let present = (#value in object); present; present = false) {} + } + } + """; + Assert.Single(TestHarness.ParseOrThrow(source)); + } + + [Theory] + [InlineData("for (let present = #value in object; ; ) {}")] + [InlineData("for (var present = true, other = #value in object; ; ) {}")] + [InlineData("for (#value in object; ; ) {}")] + [InlineData("for (present = #value in object; ; ) {}")] + [InlineData("for (present = false, #value in object; ; ) {}")] + [InlineData("for (let present = true && #value in object; ; ) {}")] + [InlineData("for (let present = true ? false : #value in object; ; ) {}")] + [InlineData("for (let present = #value in object ? true : false; ; ) {}")] + [InlineData("for (let present = () => #value in object; ; ) {}")] + [InlineData("for ([present] = #value in object; ; ) {}")] + public void UnparenthesizedPresenceIsRejectedInNoInInitializerPositions(string statement) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: any): void { let present: any; {{statement}} } + } + """; + var result = new Parser(new Lexer(source).ScanTokens()).Parse(); + Assert.False(result.IsSuccess); + Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Message.Contains("must be parenthesized")); + } + + [Theory] + [InlineData("for (let present = (#value in object); ; ) {}")] + [InlineData("for (let present = consume(#value in object); ; ) {}")] + [InlineData("for (let present = [#value in object]; ; ) {}")] + [InlineData("for (let present = { value: #value in object }; ; ) {}")] + [InlineData("for (let present = object[#value in object]; ; ) {}")] + [InlineData("for (let present = new Other(#value in object); ; ) {}")] + [InlineData("for (let present = true ? #value in object : false; ; ) {}")] + [InlineData("for (let present = `${#value in object}`; ; ) {}")] + [InlineData("for (let present = () => (#value in object); ; ) {}")] + [InlineData("for (let present = () => { return #value in object; }; ; ) {}")] + [InlineData("for ([present = #value in object] = array; ; ) {}")] + [InlineData("for (let present = true; #value in object; ) {}")] + [InlineData("for (let present = true; ; present = #value in object) {}")] + [InlineData("for (let key in (#value in object ? object : {})) {}")] + [InlineData("for (let item of (#value in object ? array : [])) {}")] + public void NestedInEnabledExpressionsAndOtherForClausesRemainAllowed(string statement) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: any): void { let present: any; {{statement}} } + } + """; + Assert.Single(TestHarness.ParseOrThrow(source)); + } + + [Fact] + public void ExistingOrdinaryInInitializerParsingIsPreserved() + => Assert.Single(TestHarness.ParseOrThrow("for (let present = 'value' in object; ; ) {}")); + + [Fact] + public void LexicalPrivateNamesOutsideClassBodiesAreRejected() + { + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow("#value in {};"))); + Assert.Equal("TS18016", exception.Diagnostic.TsCode); + } + + [Fact] + public void NestedClassDeclarationsCanResolveLexicalOuterPrivateNames() + => new TypeChecker().Check(TestHarness.ParseOrThrow(""" + class Outer { + #outer = 1; + make() { class Inner { has(value: any) { return #outer in value; } } return Inner; } + } + """)); + + [Theory] + [InlineData("class Owner { check(object: any): boolean { return #missing in object; } }")] + [InlineData("class Owner { value: number = 1; check(object: any): boolean { return #value in object; } }")] + [InlineData("class Base { #value: number = 1; } class Owner extends Base { check(object: any): boolean { return #value in object; } }")] + public void UndeclaredPrivateNamesCannotBeSynthesizedFromPublicOrInheritedNames(string source) + { + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + Assert.Equal("TS2339", exception.Diagnostic.TsCode); + } + + [Theory] + [InlineData("number")] + [InlineData("string")] + [InlineData("boolean")] + [InlineData("bigint")] + [InlineData("symbol")] + [InlineData("null")] + [InlineData("undefined")] + [InlineData("number | object")] + public void PrimitivePresenceReceiversAreRejected(string type) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: {{type}}): boolean { return #value in object; } + } + """; + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + Assert.Equal("TS2322", exception.Diagnostic.TsCode); + } + + [Theory] + [InlineData("", "TS2322")] + [InlineData(" extends any", "TS2322")] + [InlineData(" extends number", "TS2322")] + [InlineData(" extends number | object", "TS2322")] + public void GenericPresenceReceiversMustHaveObjectCompatibleConstraints(string constraint, string expectedCode) + { + var source = $$""" + class Owner { + #value = 1; + has(value: T) { return #value in value; } + } + """; + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + Assert.Equal(expectedCode, exception.Diagnostic.TsCode); + } + + [Fact] + public void UnknownPresenceReceiversAreRejected() + { + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(""" + class Owner { #value = 1; has(value: unknown) { return #value in value; } } + """))); + Assert.Equal("TS18046", exception.Diagnostic.TsCode); + } + + [Theory] + [InlineData("object")] + [InlineData("{}")] + public void ObjectCompatibleGenericConstraintsAreAllowed(string constraint) + => new TypeChecker().Check(TestHarness.ParseOrThrow($$""" + class Owner { #value = 1; has(value: T) { return #value in value; } } + """)); +} diff --git a/tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs b/tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs new file mode 100644 index 000000000..b6274c266 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs @@ -0,0 +1,24 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class EventsClassStaticIdentityTests +{ + [Theory, ModeData] + public void ImportedConstructorAndHelpersShareDefaultListenerState(ExecutionMode mode) + { + var files = new Dictionary + { + ["main.ts"] = """ + import { EventEmitter as Emitter, setMaxListeners, getMaxListeners } from 'events'; + setMaxListeners(7); + console.log(Emitter.defaultMaxListeners, getMaxListeners(null)); + Emitter.defaultMaxListeners = 12; + console.log(Emitter.defaultMaxListeners, getMaxListeners(null)); + setMaxListeners(10); + """ + }; + Assert.Equal("7 7\n12 12\n", TestHarness.RunModules(files, "main.ts", mode)); + } +} diff --git a/tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs b/tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs new file mode 100644 index 000000000..d780c5f4a --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs @@ -0,0 +1,159 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class GenericConstructorCompatibilityTests +{ + public static IEnumerable PositiveFixtures() + { + yield return ["public-pair.ts", "7\n"]; + yield return ["public-reassignment.ts", "first:1\nsecond:2\n"]; + yield return ["constrained-pair.ts", "ok\n"]; + } + + private static string Fixture(string file) + { + var root = new DirectoryInfo(AppContext.BaseDirectory); + while (root is not null && !File.Exists(Path.Combine(root.FullName, "SharpTS.sln"))) root = root.Parent; + Assert.NotNull(root); + return File.ReadAllText(Path.Combine(root.FullName, "tests", "fixtures", "GenericConstructorCompatibility", file)); + } + + [Theory, MemberData(nameof(PositiveFixtures))] + public void RetainedPositiveFixturesMatchBothEnginesAndSerializedOutput(string file, string expected) + { + var source = Fixture(file); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("private-pair.ts")] + [InlineData("static-pair.ts")] + [InlineData("narrower-constraint.ts")] + [InlineData("result-pair.ts")] + public void RetainedNegativeFixturesRemainRejected(string file) + => Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow(Fixture(file)))); + + [Theory, ModeData] + public void AliasesArgumentsReturnsAndInheritedConstructorsUseTheSelectedValue(ExecutionMode mode) + { + const string source = """ + class Base { protected brand: number = 1; value: T; + constructor(value: T) { this.value = value; } + read(): T { return this.value; } } + class Box extends Base { static tag: string = "box"; } + class Other extends Base { static tag: string = "other"; } + function identity(value: typeof Box): typeof Box { return value; } + let Alias: typeof Box = Box; + Alias = Alias; + Alias = identity(Other); + console.log(new Alias(7).read(), Alias.tag); + """; + Assert.Equal("7 other\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void OptionalDefaultAndRestConstructorsPreserveRuntimeBinding(ExecutionMode mode) + { + const string source = """ + const Box = class { value: T; constructor(value: T, ...rest: number[]) { this.value = value; } + read(): T { return this.value; } }; + const Other = class { value: U; constructor(value: U, count: number = 2) { this.value = value; } + read(): U { return this.value; } }; + let Alias: typeof Box = Other; + console.log(new Alias(7, 2).read()); + """; + Assert.Equal("7\n", TestHarness.Run(source, mode)); + } + + [Theory] + [InlineData("private value: number = 1;", "private value: number = 1;")] + [InlineData("protected value: number = 1;", "protected value: number = 1;")] + [InlineData("#value: number = 1;", "#value: number = 1;")] + [InlineData("static private value: number = 1;", "static private value: number = 1;")] + [InlineData("static protected value: number = 1;", "static protected value: number = 1;")] + [InlineData("static #value: number = 1;", "static #value: number = 1;")] + [InlineData("#read(): number { return 1; }", "#read(): number { return 1; }")] + [InlineData("static value: number = 1;", "static value: string = 'bad';")] + [InlineData("read(): number { return 1; }", "read(): string { return 'bad'; }")] + public void SameNamedDeclarationsKeepOriginsAndCacheIsolation(string left, string right) + { + var source = $$""" + const Left = class Same { {{left}} }; + const Right = class Same { {{right}} }; + let alias: typeof Left = Left; + alias = alias; + alias = Left; + alias = Right; + """; + Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ModuleImportOrderPreservesStructuralAssignmentAndActualConstructor(bool reverse) + { + var files = new Dictionary + { + ["left.ts"] = "export const Box = class Same { value: T; constructor(value: T) { this.value = value; } };", + ["right.ts"] = "export const Box = class Same { value: U; constructor(value: U) { this.value = value; console.log('right'); } };", + ["main.ts"] = (reverse + ? "import { Box as Right } from './right'; import { Box as Left } from './left';" + : "import { Box as Left } from './left'; import { Box as Right } from './right';") + """ + let Alias: typeof Left = Left; + Alias = Alias; + Alias = Right; + console.log(new Alias(7).value); + """ + }; + Assert.Equal("right\n7\n", TestHarness.RunModules(files, "main.ts", ExecutionMode.Interpreted)); + Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + Assert.Equal("right\n7\n", TestHarness.RunModules(files, "main.ts", ExecutionMode.Compiled)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ModuleImportOrderDoesNotErasePrivateOrigins(bool reverse) + { + var files = new Dictionary + { + ["left.ts"] = "export const Box = class Same { private value: number = 1; };", + ["right.ts"] = "export const Box = class Same { private value: number = 1; };", + ["main.ts"] = (reverse + ? "import { Box as Right } from './right'; import { Box as Left } from './left';" + : "import { Box as Left } from './left'; import { Box as Right } from './right';") + + "let Alias: typeof Left = Left; Alias = Alias; Alias = Right;" + }; + Assert.Throws(() => TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory, ModeData] + public void CommonBasePrivateBrandsRemainCompatible(ExecutionMode mode) + => Assert.Equal("7\n", TestHarness.Run(""" + class Base { #brand: number = 1; value: T; + constructor(value: T) { this.value = value; } } + class Left extends Base {} + class Right extends Base {} + const Alias: typeof Left = Right; + console.log(new Alias(7).value); + """, mode)); + + [Theory] + [InlineData("constructor(value: T) {}", "constructor(value: U, extra: number) {}")] + [InlineData("constructor(value: T, ...rest: number[]) {}", "constructor(value: U, ...rest: string[]) {}")] + public void IncompatibleRequiredAndRestParametersAreRejected(string left, string right) + => Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow($$""" + class Left { {{left}} } + class Right { {{right}} } + const Alias: typeof Left = Right; + """))); +} diff --git a/tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs b/tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs new file mode 100644 index 000000000..9019fee2d --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs @@ -0,0 +1,53 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class InlineConstructorPrototypeTests +{ + public static IEnumerable Cases() + { + yield return [""" + function make() { return class {}; } + const First = make(); + const Second = make(); + console.log(First === Second); + console.log(Object.getPrototypeOf(new First()) === Object.getPrototypeOf(new Second())); + """, "false\nfalse\n"]; + yield return [""" + const events: string[] = []; + function make() { return class { + constructor(a: number = 3, b: number = 4) { events.push(a + ":" + b); } + }; } + const First = make(); + function target() { events.push("target"); return First; } + function arg(value: number) { events.push("arg" + value); return value; } + console.log(Object.getPrototypeOf(new (target())(arg(1), arg(2))) === First.prototype); + console.log(Object.getPrototypeOf(new (target())()) === First.prototype); + console.log(events.join(",")); + """, "true\ntrue\ntarget,arg1,arg2,1:2,target,3:4\n"]; + yield return [""" + class Named { + value: number; + constructor(value: number = 7) { this.value = value; } + } + const Alias: any = Named; + let calls = 0; + function value() { calls++; return 9; } + console.log(Object.getPrototypeOf(new Alias(value())) === Named.prototype); + console.log(Object.getPrototypeOf(new Alias()) === Named.prototype); + console.log(new Alias().value, calls); + """, "true\ntrue\n7 1\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void InlineQueriesPreservePrototypeAndEvaluationOrderInVerifiedAssemblies(string source, string expected) + { + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } +} diff --git a/tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs b/tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs new file mode 100644 index 000000000..e949255da --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs @@ -0,0 +1,157 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class NamespaceClassIdentityTests +{ + private static string Fixture(string file) + { + var root = new DirectoryInfo(AppContext.BaseDirectory); + while (root is not null && !File.Exists(Path.Combine(root.FullName, "SharpTS.sln"))) root = root.Parent; + Assert.NotNull(root); + return File.ReadAllText(Path.Combine(root.FullName, "tests", "fixtures", "NamespaceClassIdentity", file)); + } + + [Theory] + [InlineData("siblings.ts")] + [InlineData("siblings-nongeneric.ts")] + public void RetainedSiblingFixturesPreserveOwners(string file) + { + var source = Fixture(file); + Assert.Equal("1 2 9\n", TestHarness.RunInterpreted(source)); + Assert.Equal("1 2 9\n", TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal("1 2 9\n", output); + } + + [Theory] + [InlineData("main-left.ts")] + [InlineData("main-right.ts")] + public void RetainedModuleFixturesPreserveOwnersInBothImportOrders(string entry) + { + var files = new Dictionary + { + ["left.ts"] = Fixture("left.ts"), + ["right.ts"] = Fixture("right.ts"), + [entry] = Fixture(entry) + """ + const L = Left.Box, R = Right.Box; + console.log(L === R, L.name, R.name); + console.log(new L().read(), new R().read()); + """ + }; + const string expected = "1 2\nfalse Box Box\n1 2\n"; + Assert.Equal(expected, TestHarness.RunModules(files, entry, ExecutionMode.Interpreted)); + Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, entry)); + Assert.Equal(expected, TestHarness.RunModules(files, entry, ExecutionMode.Compiled)); + } + + [Theory, ModeData] + public void NestedReopenedNamespacesKeepPrivateStorageAndBareClassReferences(ExecutionMode mode) + { + const string source = """ + namespace Left { + export let value = 1; + export class Box { + #private: number = 10; + value: T; + constructor(value: T) { this.value = value; } + read() { return value + this.#private; } + again(value: T) { return new Box(value); } + } + export namespace Nested { export class Box { read() { return 3; } } } + } + namespace Left { export class Other extends Box { read() { return super.read() + 1; } } } + namespace Right { + export let value = 2; + export class Box { + #private: number = 20; + value: T; + constructor(value: T) { this.value = value; } + read() { return value + this.#private; } + again(value: T) { return new Box(value); } + } + } + class Box { read() { return 9; } } + const l = new Left.Box(7), r = new Right.Box("ok"); + console.log(l.read(), r.read(), l.again(8).read(), r.again("next").read()); + console.log(l.value, r.value, new Left.Nested.Box().read(), new Left.Other(5).read(), new Box().read()); + """; + Assert.Equal("11 22 11 22\n7 ok 3 12 9\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ModuleMemberBodiesResolveTheirOwnNamespaceInBothImportOrders(ExecutionMode mode) + { + foreach (var reverse in new[] { false, true }) + { + var files = new Dictionary + { + ["left.ts"] = """ + export namespace Library { + export let value = 1; + export class Box { + #value = 10; + read() { return value + this.#value; } + again() { return new Box(); } + static read() { return value; } + } + } + """, + ["right.ts"] = """ + export namespace Library { + export let value = 2; + export class Box { + #value = 20; + read() { return value + this.#value; } + again() { return new Box(); } + static read() { return value; } + } + } + """, + ["main.ts"] = (reverse + ? "import { Library as R } from './right'; import { Library as L } from './left';" + : "import { Library as L } from './left'; import { Library as R } from './right';") + """ + const l = new L.Box(), r = new R.Box(); + console.log(l.again().read(), r.again().read(), L.Box.read(), R.Box.read()); + """ + }; + Assert.Equal("11 22 1 2\n", TestHarness.RunModules(files, "main.ts", mode)); + } + } + + [Theory] + [InlineData("private value = 1;")] + [InlineData("protected value = 1;")] + [InlineData("#value = 1;")] + public void SiblingNamespacesKeepDistinctPrivateOrigins(string member) + => Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow($$""" + namespace Left { export class Box { {{member}} } } + namespace Right { export class Box { {{member}} } } + let alias: typeof Left.Box = Left.Box; + alias = Right.Box; + """))); + + [Theory, ModeData] + public void TypedSiblingMethodsAndStaticMembersKeepDeclarationMetadata(ExecutionMode mode) + => Assert.Equal("7 ok\n11 22\n", TestHarness.Run(""" + namespace Left { export class Box { + static value: number = 11; + value: number; constructor(value: number) { this.value = value; } + read(): number { return this.value; } + get result(): number { return this.value; } + } } + namespace Right { export class Box { + static value: number = 22; + value: string; constructor(value: string) { this.value = value; } + read(): string { return this.value; } + get result(): string { return this.value; } + } } + const l: Left.Box = new Left.Box(7), r: Right.Box = new Right.Box("ok"); + console.log(l.result, r.read()); + console.log(Left.Box.value, Right.Box.value); + """, mode)); +} diff --git a/tests/SharpTS.Tests/SharedTests/PrivateInTests.cs b/tests/SharpTS.Tests/SharedTests/PrivateInTests.cs new file mode 100644 index 000000000..3987b127c --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/PrivateInTests.cs @@ -0,0 +1,230 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class PrivateInTests +{ + public static IEnumerable Cases() + { + yield return ["instance-brand", """ + class Base { #field = 1; #method() {} static has(value: any) { console.log(#field in value, #method in value); } } + class Derived extends Base { #field = 2; } + class Other { #field = 3; } + const value = new Base(); + Base.has(value); Base.has(new Derived()); Base.has(new Other()); + Base.has({ '#field': 1 }); Base.has(new Proxy(value, {})); + """, "true true\ntrue true\nfalse false\nfalse false\nfalse false\n"]; + yield return ["static-brand", """ + class Box { static #field = 1; static #method() {} static has(value: any) { console.log(#field in value, #method in value); } } + class Derived extends Box {} + const Alias = Box; Box.has(Box); Box.has(Alias); Box.has(Derived); Box.has(new Box()); Box.has(() => {}); + """, "true true\ntrue true\nfalse false\nfalse false\nfalse false\n"]; + yield return ["operand-once-and-primitives", """ + class Box { #field = 1; static has(value: any) { return #field in value; } } + let count = 0; + function next() { count++; return new Box(); } + console.log(Box.has(next()), count); + let errors = 0; + for (const value of [null, undefined, 0, false, "", 1n, Symbol("x")]) { + try { Box.has(value); } catch (error) { if (error instanceof TypeError) errors++; } + } + console.log(errors); + """, "true 1\n7\n"]; + yield return ["instance-initialization-order", """ + class Box { + first = console.log(#later in this, #early in this, #method in this); + #early = console.log(#later in this, #early in this, #method in this); + middle = console.log(#later in this, #early in this); + #later; + #method() {} + last = console.log(#later in this, #early in this); + } + new Box(); + """, "false false true\nfalse false true\nfalse true\ntrue true\n"]; + yield return ["static-initialization-order", """ + class Box { + static first = console.log(#later in this, #method in this); + static #early = console.log(#early in this, #later in this); + static middle = console.log(#early in this, #later in this); + static #later; + static #method() {} + static last = console.log(#early in this, #later in this); + } + """, "false true\nfalse false\ntrue false\ntrue true\n"]; + yield return ["premature-static-private-writes", """ + let calls = 0; + function next() { calls++; return 7; } + class Box { + static first = Box.probe(); + static #later = 2; + static probe() { + let threw = false; + try { this.#later = next(); } catch (error) { threw = error instanceof TypeError; } + console.log(threw, #later in this, calls); + try { this.#later = (() => { throw new Error("rhs"); })(); } + catch (error) { console.log(error.message); } + } + static after() { + const value = this.#later = next(); + console.log(value, this.#later, #later in this, calls); + } + } + Box.after(); + """, "true false 1\nrhs\n7 7 true 2\n"]; + yield return ["fresh-premature-static-private-writes", """ + let calls = 0; + function next() { calls++; return 7; } + function make(value: number) { return class Named { + static first = Named.probe(); + static #later = value; + static probe() { + let threw = false; + try { this.#later = next(); } catch (error) { threw = error instanceof TypeError; } + console.log(threw, #later in this, calls); + } + static after() { + console.log(this.#later, #later in this); + this.#later = next(); + console.log(this.#later, #later in this, calls); + } + }; } + const First = make(3), Second = make(5); + First.after(); Second.after(); + """, "true false 1\ntrue false 2\n3 true\n7 true 3\n5 true\n7 true 4\n"]; + yield return ["fresh-definition-brands", """ + function make() { return class { + #field = 1; #method() {} + static #value = 2; static #staticMethod() {} + static has(object: any) { return #value in object && #staticMethod in object; } + has(object: any) { return #field in object && #method in object; } + }; } + const First = make(), Second = make(), one = new First(), two = new Second(); + console.log(one.has(one), one.has(two), two.has(two), two.has(one)); + console.log(First.has(First), First.has(Second), Second.has(Second), Second.has(First)); + """, "true false true false\ntrue false true false\n"]; + yield return ["closures-and-suspension", """ + class Box { + #field = 1; + probe() { return (object: any) => #field in object; } + async later(object: any) { return #field in await new Promise(resolve => setTimeout(() => resolve(object), 1)); } + *values(object: any) { yield #field in object; return #field in (yield object); } + async *delayed(object: any) { yield #field in await Promise.resolve(object); } + } + async function run() { + const box = new Box(), other = new Box(), probe = box.probe(); + console.log(probe(box), probe(other), probe({})); + console.log(await box.later(box), await box.later({})); + const it = box.values(box); console.log(it.next().value); it.next(); console.log(it.next({}).value); + console.log((await box.delayed(box).next()).value); + } + run(); + """, "true true false\ntrue false\ntrue\nfalse\ntrue\n"]; + yield return ["expression-field-order", """ + const Box = class { + first = console.log(#field in this, #method in this); + #field = console.log(#field in this); + #method() {} + last = console.log(#field in this); + }; + new Box(); + """, "false true\nfalse\ntrue\n"]; + yield return ["nested-lexical-names-and-shadowing", """ + class Outer { + #outer = 1; #method() {} #same = 2; static #static = 3; + make() { return class Inner { + #same() {} + has(value: any) { return #outer in value && #method in value; } + shadows(value: any) { return #same in value; } + hasStatic(value: any) { return #static in value; } + }; } + } + const outer = new Outer(), Inner = outer.make(), inner = new Inner(); + console.log(inner.has(outer), inner.has(inner), inner.shadows(outer), inner.shadows(inner)); + console.log(inner.hasStatic(Outer), inner.hasStatic(Inner)); + """, "true false false true\ntrue false\n"]; + yield return ["nested-fresh-lexical-owners", """ + function make() { return class Outer { + #outer = 1; static #static = 2; + make() { return class Middle { + make() { return class Inner { + has(value: any) { return #outer in value; } + hasStatic(value: any) { return #static in value; } + }; } + }; } + }; } + const First = make(), Second = make(), first = new First(), second = new Second(); + const Middle = first.make(), Inner = new Middle().make(), inner = new Inner(); + console.log(inner.has(first), inner.has(second)); + console.log(inner.hasStatic(First), inner.hasStatic(Second)); + """, "true false\ntrue false\n"]; + yield return ["derived-expression-private-brands", """ + function make() { return class Base { + #field = 1; #method() {} + has(value: any) { return #field in value && #method in value; } + }; } + const First = make(), Second = make(); + const Child = class extends First {}; + const child = new Child(); + console.log(child.has(child), new First().has(child), new Second().has(child)); + """, "true true false\n"]; + yield return ["generic-static-closure-and-suspension", """ + class Box { + static #field = 1; static #method() {} + static probe() { return (value: any) => #field in value && #method in value; } + static async later(value: any) { return #field in await Promise.resolve(value); } + static *values(value: any) { yield #field in value; return #method in (yield value); } + static async *delayed(value: any) { yield #field in await Promise.resolve(value); } + } + class Child extends Box {} + async function run() { + const probe = Box.probe(); console.log(probe(Box), probe(Child), probe({})); + console.log(await Box.later(Box), await Box.later(Child)); + const it = Box.values(Box); console.log(it.next().value); it.next(); console.log(it.next(Child).value); + console.log((await Box.delayed(Box).next()).value); + } + run(); + """, "true false false\ntrue false\ntrue\nfalse\ntrue\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void BrandChecksMatchAcrossEngines(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + // Proxy explicitly requires the deployed runtime; verification/run above + // supplies it. The other cases also enforce dependency-free deployment. + if (name != "instance-brand") Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } + + [Theory, ModeData] + public void ModuleNamesKeepTheirLexicalBrands(ExecutionMode mode) + { + var files = new Dictionary + { + ["left.ts"] = "export class Box { #field = 1; static has(x: any) { return #field in x; } }", + ["right.ts"] = "export class Box { #field = 1; static has(x: any) { return #field in x; } }", + ["main.ts"] = """ + import { Box as Left } from './left'; import { Box as Right } from './right'; + console.log(Left.has(new Left()), Left.has(new Right()), Right.has(new Right())); + """ + }; + Assert.Equal("true false true\n", TestHarness.RunModules(files, "main.ts", mode)); + if (mode == ExecutionMode.Compiled) Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory] + [InlineData("class Box { #field = 1; has() { return #field in 1; } }")] + [InlineData("class Box { has(x: any) { return #missing in x; } }")] + [InlineData("const x = #field in {};")] + public void InvalidNamesOrPrimitiveTypesAreRejected(string source) + { + Assert.Throws(() => TestHarness.RunInterpreted(source)); + Assert.Throws(() => TestHarness.RunCompiled(source)); + } +} diff --git a/tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs b/tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs new file mode 100644 index 000000000..381f38b9e --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs @@ -0,0 +1,216 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class PrivateMethodValueTests +{ + public static IEnumerable Cases() + { + yield return ["public-property-write-and-call", """ + class Box { + value = 3; + read() { return this.value; } + #change() { this.value = this.value + 1; return this.read(); } + extract() { return this.#change; } + } + const box = new Box(), change = box.extract(); + const object = { value: 7, read() { return this.value + 10; } }; + console.log(change.call(object), object.value, box.value); + """, "18 8 3\n"]; + yield return ["real-suspension-and-receiver-order", """ + let reads = 0; + class Box { + value = 3; + #read(amount = 2) { return this.value + amount; } + async #later() { await new Promise(resolve => setTimeout(() => resolve(0), 1)); return this.value; } + async extract(object: any) { return (await Promise.resolve(object)).#read; } + later() { return this.#later; } + } + function receiver(object: any) { reads++; return object; } + async function run() { + const box = new Box(), read = await box.extract(receiver(box)), later = box.later(); + console.log(reads, read === await box.extract(box)); + console.log(read.call({ value: 7 }, await new Promise(resolve => setTimeout(() => resolve(5), 1)))); + console.log(await later.call({ value: 11 })); + } + run().catch(error => console.log("error", error)); + """, "1 true\n12\n11\n"]; + yield return ["generic-static-values", """ + class Box { + static #value = 7; + static async #read() { await Promise.resolve(0); return this.#value; } + static extract() { return Box.#read; } + } + class Other { static #value = 9; } + async function run() { + const read = Box.extract(); + console.log(read === Box.extract(), await read.call(Box)); + try { await read.call(Other); } catch (error) { console.log(error instanceof TypeError); } + } + run().catch(error => console.log("error", error)); + """, "true 7\ntrue\n"]; + yield return ["repeated-static-private-state", """ + function make(value: number) { + return class { + static #value = value; + static #read() { return this.#value; } + static extract() { return this.#read; } + }; + } + const First = make(3), Second = make(7), one = First.extract(), two = Second.extract(); + console.log(one === First.extract(), one === two, one.call(First), two.call(Second)); + try { one.call(Second); } catch (error) { console.log(error instanceof TypeError); } + """, "true false 3 7\ntrue\n"]; + yield return ["repeated-class-evaluations", """ + function make(value: number) { + return class Named { + #value = value; + #read() { return this.#value; } + #captured() { return value; } + extract(object: any) { return object.#read; } + captured() { return this.#captured; } + call() { return this.#read(); } + }; + } + const First = make(3), Second = make(7), first = new First(), second = new Second(); + const read = first.extract(first), other = second.extract(second); + console.log(read === first.extract(new First()), read === other, read.call(first), first.call()); + console.log(first.captured().call(second), second.captured().call({})); + try { first.extract(second); } catch (error) { console.log(error instanceof TypeError); } + try { read.call(second); } catch (error) { console.log(error instanceof TypeError); } + """, "true false 3 3\n3 7\ntrue\ntrue\n"]; + yield return ["closures-and-static-suspension", """ + class Box { + value = 3; + #read(): () => number { return () => this.value; } + async extract(object: any) { await Promise.resolve(0); return () => object.#read; } + static async #later() { await Promise.resolve(0); return this; } + static *#values() { yield this; } + static async *#more() { await Promise.resolve(0); yield this; } + static values() { return [this.#later, this.#values, this.#more]; } + } + async function run() { + const first = new Box(), read = (await first.extract(first))(); + console.log(read.call({ value: 11 })()); + const values: any = Box.values(); + const later = values[0], stream = values[1], more = values[2]; + console.log(await later() === undefined, stream().next().value === undefined); + console.log((await more().next()).value === undefined); + } + run().catch(error => console.log("error", error)); + """, "11\ntrue true\ntrue\n"]; + yield return ["identity-and-receivers", """ + class Box { + value: number; + constructor(value: number) { this.value = value; } + #read(): number { return this.value; } + extract() { return this.#read; } + } + const first = new Box(3), second = new Box(7), read = first.extract(); + console.log(read === first.extract(), read === second.extract()); + console.log(read.call(second), read.apply({ value: 11 }, []), read.bind(second)()); + console.log(read.name, read.length); + try { new (read as any)(); } catch (error) { console.log(error instanceof TypeError); } + """, "true true\n7 11 7\n#read 0\ntrue\n"]; + yield return ["generic-identity", """ + class Box { + value: T; + constructor(value: T) { this.value = value; } + #read(): T { return this.value; } + extract() { return this.#read; } + } + const first = new Box(3), second = new Box("seven"), read = first.extract(); + console.log(read === first.extract(), (read as any) === second.extract()); + console.log(read.call(second), read.call({ value: 11 })); + """, "true true\nseven 11\n"]; + yield return ["brands-and-strict-this", """ + let reads = 0; + class Box { + #value = 3; + #read() { return this.#value; } + #receiver() { return this; } + extract(object: any) { return object.#read; } + receiver() { return this.#receiver; } + } + const first = new Box(), read = first.extract(first); + console.log(read.call(first)); + try { first.extract((reads++, {})); } catch (error) { console.log(error instanceof TypeError, reads); } + try { read.call({}); } catch (error) { console.log(error instanceof TypeError); } + try { read.call(null); } catch (error) { console.log(error instanceof TypeError); } + const receiver = first.receiver(); + console.log(receiver() === undefined, receiver.call(null) === null); + """, "3\ntrue 1\ntrue\ntrue\ntrue true\n"]; + yield return ["static-and-defaults", """ + class Box { + static value = 7; + static #read(first = 2, ...rest: any[]) { return this.value + first + rest.length; } + static extract(object: any) { return object.#read; } + } + const read = Box.extract(Box); + console.log(read === Box.extract(Box), read.name, read.length); + console.log(read.call({ value: 11 }), read.apply({ value: 11 }, [3, 4, 5])); + try { Box.extract({}); } catch (error) { console.log(error instanceof TypeError); } + """, "true #read 0\n13 16\ntrue\n"]; + yield return ["suspended-method-kinds", """ + class Box { + value: T; + constructor(value: T) { this.value = value; } + async #read() { await Promise.resolve(0); return this.value; } + *#values() { yield this.value; } + async *#later() { await Promise.resolve(0); yield this.value; } + extract() { return [this.#read, this.#values, this.#later]; } + } + async function run() { + const first = new Box(3), second = new Box("seven"); + const values: any = first.extract(), other: any = second.extract(); + console.log(values[0] === other[0], values[1] === other[1], values[2] === other[2]); + console.log(await values[0].call({ value: 11 })); + console.log(values[1].call(second).next().value); + console.log((await values[2].call({ value: 13 }).next()).value); + } + run(); + """, "true true true\n11\nseven\n13\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void PrivateMethodsHaveCanonicalUnboundValues(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } + + [Theory, ModeData] + public void ModulePrivateMethodValuesBelongToTheirDeclaration(ExecutionMode mode) + { + var files = new Dictionary + { + ["left.ts"] = "export class Box { #read() { return 3; } extract() { return this.#read; } }", + ["right.ts"] = "export class Box { #read() { return 7; } extract() { return this.#read; } }", + ["main.ts"] = """ + import { Box as Left } from './left'; + import { Box as Right } from './right'; + const left = new Left().extract(), other = new Left().extract(), right = new Right().extract(); + console.log(left === other, left === right, left(), right()); + """ + }; + Assert.Equal("true false 3 7\n", TestHarness.RunModules(files, "main.ts", mode)); + if (mode == ExecutionMode.Compiled) Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory] + [InlineData("class Box { #read() {} } new Box().#read;")] + [InlineData("class Box { #read() {} replace() { this.#read = () => {}; } }")] + [InlineData("class Box { extract() { return this.#missing; } }")] + public void InvalidPrivateMethodAccessIsRejected(string source) + { + Assert.Throws(() => TestHarness.RunInterpreted(source)); + Assert.Throws(() => TestHarness.RunCompiled(source)); + } +} diff --git a/tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs b/tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs new file mode 100644 index 000000000..c8090e577 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs @@ -0,0 +1,78 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RepeatedClassAsyncGeneratorCaptureTests +{ + public static IEnumerable Cases() + { + yield return ["instance-arrow-before-await", """ + function make(seed: number) { + return class { + async *values(): AsyncGenerator { + const value = 100; + { + const value = seed; + const read = () => value; + await new Promise(resolve => setTimeout(() => resolve(0), 1)); + yield read; + } + yield value; + yield () => seed; + } + }; + } + async function run() { + const First = make(7), Second = make(9); + const first = new First().values(), second = new Second().values(); + const one = await first.next(), two = await second.next(); + console.log(one.value(), two.value()); + console.log((await first.next()).value, (await second.next()).value); + const outerOne = await first.next(), outerTwo = await second.next(); + console.log(outerOne.value(), outerTwo.value(), one.value(), two.value()); + } + run(); + """, "7 9\n100 100\n7 9 7 9\n"]; + yield return ["static-arrow-after-await", """ + function make(seed: number) { + return class { + static async *values(): AsyncGenerator { + const value = 100; + { + const value = seed; + await new Promise(resolve => setTimeout(() => resolve(0), 1)); + yield () => value; + } + yield value; + yield () => seed; + yield this; + } + }; + } + async function run() { + const First = make(7), Second = make(9); + const first = First.values(), second = Second.values(); + const one = await first.next(), two = await second.next(); + console.log(one.value(), two.value()); + console.log((await first.next()).value, (await second.next()).value); + const outerOne = await first.next(), outerTwo = await second.next(); + console.log(outerOne.value(), outerTwo.value(), one.value(), two.value()); + console.log((await first.next()).value === First, (await second.next()).value === Second); + } + run(); + """, "7 9\n100 100\n7 9 7 9\ntrue true\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void DefinitionAndShadowCapturesSurviveSuspension(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } +} diff --git a/tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs b/tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs new file mode 100644 index 000000000..d9a232497 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs @@ -0,0 +1,122 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RepeatedClassDefinitionTests +{ + public static IEnumerable Cases() + { + yield return ["function", """ + function make() { return class Named {}; } + const First = make(), Second = make(); + const first = new First(), second = new Second(); + const fp = Object.getPrototypeOf(first), sp = Object.getPrototypeOf(second); + console.log(First === Second, fp === sp); + console.log(fp === First.prototype, sp === Second.prototype); + console.log(first.constructor === First, second.constructor === Second); + console.log(first instanceof First, first instanceof Second, second instanceof First, second instanceof Second); + console.log(First.name, Second.name, First.length, typeof First); + """, "false false\ntrue true\ntrue true\ntrue false false true\nNamed Named 0 function\n"]; + yield return ["generic", """ + function make() { return class Box { value: T; constructor(value: T) { this.value = value; } }; } + const First = make(), Second = make(); + const one = new First(7), two = new First("ok"), three = new Second(9); + const op = Object.getPrototypeOf(one), tp = Object.getPrototypeOf(two), hp = Object.getPrototypeOf(three); + console.log(op === tp, op === hp, one.constructor === First, two.constructor === First); + console.log(one.value, two.value, three.value, First.length); + """, "true false true true\n7 ok 9 1\n"]; + yield return ["field-initialization", """ + function make() { return class Named { + owner: any = this.constructor; + prototype: any = Object.getPrototypeOf(this); + }; } + const First = make(), Second = make(); + const first = new First(), second = new Second(); + console.log(first.owner === First, second.owner === Second); + console.log(first.prototype === First.prototype, second.prototype === Second.prototype); + """, "true true\ntrue true\n"]; + yield return ["loop-closure", """ + const constructors: any[] = []; + function outer() { return () => class Named {}; } + const make = outer(); + for (let i = 0; i < 3; i++) constructors.push(make()); + console.log(constructors[0] === constructors[1], constructors[1] === constructors[2]); + const first = new constructors[0](), last = new constructors[2](); + console.log(first.constructor === constructors[0], last.constructor === constructors[2]); + """, "false false\ntrue true\n"]; + yield return ["generator", """ + function* make() { for (let i = 0; i < 2; i++) { yield class Named {}; } } + const iterator = make(); + const First: any = iterator.next().value, Second: any = iterator.next().value; + console.log(First === Second); + const first = new First(), second = new Second(); + console.log(first.constructor === First, second.constructor === Second); + """, "false\ntrue true\n"]; + yield return ["async", """ + async function make() { await Promise.resolve(0); return class Named {}; } + async function run() { + const First = await make(), Second = await make(); + console.log(First === Second); + const first = new First(), second = new Second(); + console.log(first.constructor === First, second.constructor === Second); + } + run(); + """, "false\ntrue true\n"]; + yield return ["reflect", """ + function make() { return class Named { value: number; constructor(value: number) { this.value = value; } }; } + const First = make(), Second = make(); + const first: any = Reflect.construct(First, [7]), second: any = Reflect.construct(Second, [9]); + console.log(first.value, second.value, first.constructor === First, second.constructor === Second); + console.log(first instanceof First, first instanceof Second); + """, "7 9 true true\ntrue false\n"]; + yield return ["self-and-prototype-properties", """ + function make() { return class Named { self() { return Named; } }; } + const First = make(), Second = make(); + First.prototype.choice = 7; + const first: any = new First(), second: any = new Second(); + console.log(first.self() === First, second.self() === Second); + console.log(first.choice, second.choice); + """, "true true\n7 undefined\n"]; + yield return ["arguments-and-defaults", """ + function make() { return class Named { + value: number; count: number; + constructor(value: number = 7) { this.value = value; this.count = arguments.length; } + }; } + const First = make(), Second: any = make(); + const first = new First(), second = new Second(9, 10); + console.log(first.value, first.count, second.value, second.count, First.length); + """, "7 0 9 2 0\n"]; + yield return ["bound-construction", """ + function make() { return class Named { value: number; constructor(value: number) { this.value = value; } }; } + const First = make(), Second = make(); + const Bound: any = First.bind(null, 7); + const first: any = new Bound(); + console.log(first.value, first.constructor === First, first instanceof First, first instanceof Second); + """, "7 true true false\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void EvaluationsOwnConstructorAndPrototypeIdentity(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + } + + [Theory, ModeData] + public void ModuleFactoryKeepsEvaluationIdentityAcrossCalls(ExecutionMode mode) + => Assert.Equal("false true true\n", TestHarness.RunModules(new() + { + ["factory.ts"] = "export function make() { return class Named {}; }", + ["main.ts"] = """ + import { make } from './factory'; + const First = make(), Second = make(); + const first = new First(), second = new Second(); + console.log(First === Second, first.constructor === First, second.constructor === Second); + """ + }, "main.ts", mode)); +} diff --git a/tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs b/tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs new file mode 100644 index 000000000..e95bb1a0d --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs @@ -0,0 +1,248 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RepeatedClassStateTests +{ + public static IEnumerable Cases() + { + yield return ["retained-generic-field-keys", """ + let count = 0; + function key() { count++; return "key" + count; } + function make() { return class { [key()] = 5; }; } + const First = make(); + const early: any = new First(); + const Second = make(); + const late: any = new First(); + const second: any = new Second(); + console.log(First === Second, count); + console.log(early.key1, late.key1, late.key2, second.key1, second.key2); + """, "false 2\n5 5 undefined undefined 5\n"]; + yield return ["symbol-field-keys", """ + const left = Symbol("key"), right = Symbol("key"); + function make(key: symbol) { return class { [key] = 7; }; } + const First = make(left), Second = make(right); + const first: any = new First(), second: any = new Second(); + console.log(first[left], first[right], second[left], second[right]); + console.log(Reflect.ownKeys(first)[0] === left, Reflect.ownKeys(second)[0] === right); + """, "7 undefined undefined 7\ntrue true\n"]; + yield return ["awaited-field-keys", """ + async function make(key: string) { return class { [await Promise.resolve(key)] = 5; }; } + async function run() { + const First = await make("left"), Second = await make("right"); + const first: any = new First(), second: any = new Second(); + console.log(first.left, first.right, second.left, second.right); + } + run(); + """, "5 undefined undefined 5\n"]; + yield return ["captured-method-values", """ + function make(value: number) { return class { read() { return value; } }; } + const First = make(7), Second = make(9); + console.log(new First().read(), new Second().read(), new First().read()); + """, "7 9 7\n"]; + yield return ["inherited-field-keys", """ + function make(key: string) { return class { [key] = 7; }; } + const First = make("left"), Second = make("right"); + const Child = class extends First {}; + const Other = class extends Second { constructor() { super(); } }; + const first: any = new Child(), second: any = new Other(); + console.log(first.left, first.right, second.left, second.right); + """, "7 undefined undefined 7\n"]; + yield return ["static-initialization", """ + let count = 0; + function make() { return class { static value = ++count; }; } + const First = make(), Second = make(); + console.log(count, First.value, Second.value); + const first = new First(), second = new Second(); + console.log(count, first.constructor === First, second.constructor === Second); + """, "2 1 2\n2 true true\n"]; + yield return ["computed-methods-and-accessors", """ + function make(key: string, value: number) { + return class { [key]() { return value; } get ["get" + key]() { return value + 1; } }; + } + const First = make("left", 7), Second = make("right", 9); + const first: any = new First(), second: any = new Second(); + console.log(first.left(), first.getleft, first.right, first.getright); + console.log(second.right(), second.getright, second.left, second.getleft); + const later: any = new First(); + console.log(later.left()); + """, "7 8 undefined undefined\n9 10 undefined undefined\n7\n"]; + yield return ["symbol-methods-and-accessors", """ + const left = Symbol("method"), right = Symbol("method"), leftGet = Symbol("getter"), rightGet = Symbol("getter"); + function make(methodKey: symbol, getterKey: symbol, value: number) { + return class { [methodKey]() { return value; } get [getterKey]() { return value + 1; } }; + } + const First = make(left, leftGet, 7), Second = make(right, rightGet, 9); + const first: any = new First(), second: any = new Second(); + console.log(first[left](), first[leftGet], first[right], first[rightGet]); + console.log(second[right](), second[rightGet], second[left], second[leftGet]); + """, "7 8 undefined undefined\n9 10 undefined undefined\n"]; + yield return ["static-captures-and-receivers", """ + function make(value: number) { + return class Named { + static value = value; + static read() { return value; } + static get captured() { return value; } + static receiver() { return this; } + static self() { return Named; } + }; + } + const First = make(7), Second = make(9); + First.value = 11; + console.log(First.value, Second.value, First.read(), Second.read(), First.captured, Second.captured); + console.log(First.receiver() === First, First.self() === First, Second.self() === Second); + const borrowed = First.receiver; + const other = {}; + console.log(borrowed.call(other) === other, First.read.call(other)); + """, "11 9 7 9 7 9\ntrue true true\ntrue 7\n"]; + yield return ["key-and-static-source-order", """ + const trace: string[] = []; + function key(label: string) { trace.push("key:" + label); return label; } + function value(label: string) { trace.push("init:" + label); return label; } + function make() { + return class { + static [key("first")] = value("first"); + static { trace.push("block"); this.block = 3; } + static [key("last")] = value("last"); + }; + } + const First: any = make(), Second: any = make(); + console.log(trace.join(",")); + console.log(First.first, First.last, First.block, Second.first, Second.last, Second.block); + """, "key:first,key:last,init:first,block,init:last,key:first,key:last,init:first,block,init:last\nfirst last 3 first last 3\n"]; + yield return ["interleaved-await-captures", """ + async function make(key: string, value: number) { + return class { [await Promise.resolve(key)] = value; read() { return value; } }; + } + async function run() { + const one = make("left", 7), two = make("right", 9); + const First = await one, Second = await two; + const first: any = new First(), second: any = new Second(); + console.log(first.left, first.right, first.read(), second.left, second.right, second.read()); + } + run(); + """, "7 undefined 7 undefined 9 9\n"]; + yield return ["abrupt-definition", """ + let count = 0; + function key(fail: boolean) { if (fail) throw new Error("key"); return "value"; } + function make(fail: boolean) { return class { static [key(fail)] = ++count; }; } + const First: any = make(false); + try { make(true); } catch (error) { console.log(error.message); } + const Second: any = make(false); + console.log(count, First.value, Second.value); + """, "key\n2 1 2\n"]; + yield return ["suspended-method-captures", """ + function make(value: number) { + return class { + *read() { yield value; } + async readAsync() { await Promise.resolve(0); return value; } + static *readStatic() { yield value; } + static async readStaticAsync() { await Promise.resolve(0); return value; } + }; + } + async function run() { + const First = make(7), Second = make(9); + console.log(new First().read().next().value, new Second().read().next().value); + console.log(First.readStatic().next().value, Second.readStatic().next().value); + console.log(await new First().readAsync(), await new Second().readAsync()); + console.log(await First.readStaticAsync(), await Second.readStaticAsync()); + } + run(); + """, "7 9\n7 9\n7 9\n7 9\n"]; + yield return ["parent-survives-awaited-key", """ + function make(key: string) { return class { [key] = 7; }; } + const First = make("left"); + async function makeChild() { return class extends First { [await Promise.resolve("right")] = 9; }; } + async function run() { + const Child = await makeChild(); + const Second = make("other"); + const child: any = new Child(); + console.log(child.left, child.right, child.other, child instanceof First, child instanceof Second); + } + run(); + """, "7 9 undefined true false\n"]; + yield return ["captured-method-closures", """ + function make(value: number) { return class { read() { return () => value; } }; } + const First = make(7), Second = make(9); + const first = new First().read(), second = new Second().read(); + console.log(first(), second(), first()); + """, "7 9 7\n"]; + yield return ["live-captured-bindings", """ + function make(value: number) { + const C = class { + read() { return value; } + write(next: number) { value = next; } + increment() { value++; value += 2; return value; } + closure() { return () => value; } + }; + value += 10; + return C; + } + const First = make(7), Second = make(9); + const first = new First(), second = new Second(), read = first.closure(); + console.log(first.read(), second.read(), read()); + first.write(21); + console.log(first.increment(), read(), second.read()); + """, "17 19 17\n24 24 19\n"]; + yield return ["suspended-method-closures", """ + function make(value: number) { + return class { async read() { await Promise.resolve(0); return () => value; } + *values() { yield () => value; } }; + } + async function run() { + const First = make(7), Second = make(9); + const one = await new First().read(), two = await new Second().read(); + console.log(one(), two(), new First().values().next().value()); + } + run(); + """, "7 9 7\n"]; + yield return ["borrowed-instance-environments", """ + function make(value: number) { + return class { field = value; read() { return value + this.field; } + async readAsync() { await Promise.resolve(0); return value; } + *values() { yield value; } }; + } + async function run() { + const First = make(7), Second = make(9), other = new Second(); + const first = new First(); + console.log(first.read.call(other), first.read.call({ field: 11 })); + console.log(await first.readAsync.call(other), first.values.call(other).next().value); + } + run(); + """, "16 18\n7 7\n"]; + } + + [Theory, ModeData] + public void SameNamedModuleFactoriesRetainTheirDefinitionState(ExecutionMode mode) + { + var files = new Dictionary + { + ["left.ts"] = "export function make(value: number) { return class Named { ['left'] = value; static value = value; read() { return value; } }; }", + ["right.ts"] = "export function make(value: number) { return class Named { ['right'] = value; static value = value; read() { return value; } }; }", + ["main.ts"] = """ + import { make as left } from './left'; + import { make as right } from './right'; + const First: any = left(7), Second: any = right(9), Third: any = left(11); + const first: any = new First(), second: any = new Second(), third: any = new Third(); + console.log(first.left, first.right, first.read(), First.value); + console.log(second.left, second.right, second.read(), Second.value); + console.log(third.left, third.right, third.read(), Third.value); + """ + }; + Assert.Equal("7 undefined 7 7\nundefined 9 9 9\n11 undefined 11 11\n", TestHarness.RunModules(files, "main.ts", mode)); + if (mode == ExecutionMode.Compiled) Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory, MemberData(nameof(Cases))] + public void DefinitionsRetainKeysCapturesAndInitialization(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } +} diff --git a/tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs b/tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs new file mode 100644 index 000000000..741f25f72 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs @@ -0,0 +1,159 @@ +using SharpTS.Tests.CompilerTests; +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RuntimeSuperclassTests +{ + [Theory] + [InlineData("awaited-parent.ts", "right\n2 right\ntrue\ntrue false\n")] + [InlineData("observed-parent.ts", "right\nfunction right\ntrue\ntrue false\n")] + [InlineData("abrupt-parent.ts", "parent \n")] + public void InvestigationFixturesMatchInBothEnginesAndVerifiedOutput(string name, string expected) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null && !Directory.Exists(Path.Combine(directory.FullName, "tests", "fixtures", "DynamicSuperclass"))) + directory = directory.Parent; + Assert.NotNull(directory); + var source = File.ReadAllText(Path.Combine(directory.FullName, "tests", "fixtures", "DynamicSuperclass", name)); + Verify(source, expected); + // Hosted modules invoke a reserved main entry point after initialization. + // These scripts already call their runner, so give that runner a neutral name. + HistoricalRuntimeDeploymentTests.AssertDeployment(name, + source.Replace("main(", "runtimeParentProbe(", StringComparison.Ordinal), expected); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void PendingParentSelectionUsesTheOriginalReceiverAndEachDefinition(bool declaration) + { + var body = """ + { + static initialized = (order += "static "); + own = 10; + constructor(value: number) { super(value); this.value += this.own; } + child() { return super.read() + this.own; } + } + """; + var definition = declaration ? $"class Child extends selected {body}; return Child;" + : $"return class extends selected {body};"; + var source = """ + let order = ""; + class Root { + value = 0; + read() { return this.value; } + } + class Left extends Root { + static side = "left"; + receiver: any; + constructor(value: number) { super(); this.value = value; this.receiver = this; } + read() { return super.read() + 1; } + } + class Right extends Root { + static side = "right"; + receiver: any; + constructor(value: number) { super(); this.value = value * 2; this.receiver = this; } + read() { return super.read() + 2; } + } + function select(parent: any) { + order += "select "; + return new Promise(resolve => setTimeout(() => { order += "settle "; resolve(parent); }, 1)); + } + async function make(parent: any) { + const selected = await select(parent); + order += "ready "; + """ + definition + """ + } + async function main() { + const First: any = await make(Left), Second: any = await make(Right); + const first: any = new First(3), second: any = new Second(4); + console.log(first.read(), first.child(), second.read(), second.child()); + console.log(first.receiver === first, second.receiver === second, first.own, second.own); + console.log(First.side, Second.side, Object.getPrototypeOf(First) === Left, Object.getPrototypeOf(Second) === Right); + console.log(Object.getPrototypeOf(First.prototype) === Left.prototype, Object.getPrototypeOf(Second.prototype) === Right.prototype); + console.log(first instanceof Root, first instanceof Left, first instanceof Right, second instanceof Root, second instanceof Left, second instanceof Right); + console.log(order); + } + main().then(() => {}, error => console.log("rejected", error.message)); + """; + Verify(source, "14 24 20 30\ntrue true 10 10\nleft right true true\ntrue true\ntrue true false true false true\nselect settle ready static select settle ready static \n"); + } + + [Theory, ModeData] + public void ImplicitDerivedConstructorForwardsArguments(ExecutionMode mode) + { + var source = """ + class Base { value = 0; constructor(value: number) { this.value = value; } read() { return this.value; } } + function make(parent: any) { return class extends parent { own = 7; }; } + const Child: any = make(Base), instance: any = new Child(9); + console.log(instance.read(), instance.own, instance instanceof Base); + """; + Assert.Equal("9 7 true\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ParentAdaptersPreserveDefaultsArgumentsAndStaticReceiver(ExecutionMode mode) + { + var source = """ + class Root { static read() { return this.side; } static side = "root"; } + class Parent extends Root { + static side = "parent"; + static read() { return this.side; } + value = 0; + count = 0; + total = 0; + constructor(value: number = 7, ...rest: number[]) { + super(); this.value = value; this.count = arguments.length; this.total = rest.length; + } + read(...values: number[]) { console.log(arguments.length, values[0]); return this.value; } + } + function make(parent: any) { return class extends parent { static side = "child"; }; } + const Child: any = make(Parent), first: any = new Child(), second: any = new Child(9, 10, 11); + console.log(first.read(3), first.count, first.total, second.read(4), second.count, second.total); + console.log(Child.read(), Parent.read(), Root.read()); + """; + Assert.Equal("1 3\n1 4\n7 0 0 9 3 2\nchild parent root\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void SelectedExpressionParentAndSameNameParameterUseRuntimeIdentity(ExecutionMode mode) + { + var source = """ + class Parent { value = 1; read() { return this.value; } } + const Selected = class { value = 2; read() { return this.value; } }; + function make(Parent: any) { return class extends Parent { own = 3; }; } + const Child: any = make(Selected), instance: any = new Child(); + console.log(instance.read(), instance.own, instance instanceof Selected, instance instanceof Parent); + console.log(Object.getPrototypeOf(Child) === Selected, Object.getPrototypeOf(Child.prototype) === Selected.prototype); + """; + Assert.Equal("2 3 true false\ntrue true\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void InvalidParentAndRejectedAwaitPrecedeStaticInitialization(ExecutionMode mode) + { + var source = """ + let initialized = ""; + const reason = new Error("original"); + function make(parent: any) { return class extends parent { static value = (initialized += "bad"); }; } + try { make(7); } catch (error: any) { console.log(error instanceof TypeError, initialized); } + async function run() { + try { class Child extends (await Promise.reject(reason)) { static value = (initialized += "bad"); } } + catch (error: any) { console.log(error === reason, error.message, initialized); } + } + run(); + """; + Assert.Equal("true \ntrue original \n", TestHarness.Run(source, mode)); + } + + private static void Verify(string source, string expected) + { + Assert.Equal(expected, TestHarness.RunInterpreted(source, TimeSpan.FromSeconds(30))); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + } +} diff --git a/tests/fixtures/PrivateIn/README.md b/tests/fixtures/PrivateIn/README.md index a97e0fd7e..20eb156a4 100644 --- a/tests/fixtures/PrivateIn/README.md +++ b/tests/fixtures/PrivateIn/README.md @@ -1,8 +1,12 @@ -# Private-in investigation controls (#1903) +# Private-in regression controls (#1962) These new sources are not recovered historical reproducers. The implementation design and recorded evidence are in `docs/plans/private-in-brand-checks.md`. Implementation is tracked by [#1962](https://github.com/nickna/SharpTS/issues/1962). +The positive controls are now exercised by `PrivateInTests` in both engines, +including serialized IL verification. CLI verification and execution use a +30-second deadline for each process. The proxy control deploys `SharpTS.dll`; +the other positive controls also run without it. Node v25.5.0 and TypeScript 7.0.2 accept the three positive sources. The two brand files each print `true true` twice followed by `false false` three times. diff --git a/tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts b/tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts new file mode 100644 index 000000000..faa8ac873 --- /dev/null +++ b/tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts @@ -0,0 +1,9 @@ +// #1964 identity control keeps prototype results in locals; identity.ts retains #1966's inline IL probe. +function make() { return class Named {}; } +const First = make(), Second = make(); +const first = new First(), second = new Second(); +const fp = Object.getPrototypeOf(first), sp = Object.getPrototypeOf(second); +console.log(First === Second); +console.log(fp === sp); +console.log(first.constructor === First, second.constructor === Second); +console.log(first instanceof First, first instanceof Second);