Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Compilation/ExpressionEmitterBase.CallHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2361,9 +2361,25 @@ protected void EmitConversionForParameter(Expr expr, Type targetType)
}

// If target is a value type and we have an object, unbox
if (targetType.IsValueType && StackType != StackType.Double && StackType != StackType.Boolean)
if (targetType.IsValueType)
{
IL.Emit(OpCodes.Unbox_Any, targetType);
if (StackType != StackType.Double && StackType != StackType.Boolean)
IL.Emit(OpCodes.Unbox_Any, targetType);
return;
}

// Target is a non-object reference type. EmitExpression can leave a broader `object` on the
// stack — an `any`-typed value, or a GetIndex/GetProperty result — that the callee's typed
// string slot would reject at IL verification even though the JIT tolerates it. Emit the
// downcast the verifier needs (#1246). Only `string` qualifies: it is the one non-object
// reference slot whose runtime value is genuinely a CLR instance of the slot. Other reference
// targets carry a boxed-differently runtime value ($TSFunction for a Delegate, $Array for a
// List, $Promise for a Task — the last widened to `object` at the parameter slot by
// ParameterTypeResolver), so a castclass would throw; they keep their prior object store.
if (Types.IsString(targetType) && StackType != StackType.String)
{
EnsureBoxed();
IL.Emit(OpCodes.Castclass, targetType);
}
}

Expand Down
20 changes: 18 additions & 2 deletions Compilation/ILEmitter.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,25 @@ or TypeInfo.Void
}

// If target is a value type and we have an object, unbox
if (targetType.IsValueType && _stackType != StackType.Double && _stackType != StackType.Boolean)
if (targetType.IsValueType)
{
IL.Emit(OpCodes.Unbox_Any, targetType);
if (_stackType != StackType.Double && _stackType != StackType.Boolean)
IL.Emit(OpCodes.Unbox_Any, targetType);
return;
}

// Target is a non-object reference type. EmitExpression can leave a broader `object` on the
// stack — an `any`-typed value, or a GetIndex/GetProperty result — that the callee's typed
// string slot would reject at IL verification even though the JIT tolerates it. Emit the
// downcast the verifier needs (#1246). Only `string` qualifies: it is the one non-object
// reference slot whose runtime value is genuinely a CLR instance of the slot. Other reference
// targets carry a boxed-differently runtime value ($TSFunction for a Delegate, $Array for a
// List, $Promise for a Task — the last widened to `object` at the parameter slot by
// ParameterTypeResolver), so a castclass would throw; they keep their prior object store.
if (_ctx.Types.IsString(targetType) && _stackType != StackType.String)
{
EmitBoxIfNeeded(expr);
IL.Emit(OpCodes.Castclass, targetType);
}
}

Expand Down
19 changes: 19 additions & 0 deletions Compilation/ParameterTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,28 @@ private static Type CoerceParamSlotType(Type mapped, TSTypeInfo source, TypeMapp
return typeof(object);
if (UnionAdmitsUndefined(source))
return typeof(object);
if (IsPromiseTaskSlot(mapped))
return typeof(object);
return mapped;
}

/// <summary>
/// True when <paramref name="mapped"/> is a CLR <c>Task</c>/<c>Task&lt;T&gt;</c> produced by
/// strictly mapping a <c>Promise&lt;T&gt;</c> annotation. A parameter so typed receives the runtime
/// <c>$Promise</c> object at the call boundary, never a real CLR <c>Task</c> (the promise
/// primitives return <c>$Promise</c>, and no path constructs a bare <c>Task</c> guest value), so
/// the slot must widen to <c>object</c>. Mirrors the identical <c>Promise&lt;T&gt;</c> → object
/// widening already applied to non-async return slots in <see cref="ResolveReturnType"/> (#393);
/// its absence on parameters left the <c>fs</c> facade callback helpers emitting a <c>$Promise</c>
/// into a <c>Task&lt;object&gt;</c> slot, which the JIT tolerates but IL verification rejects
/// (#1246). The widening is unconditional — even an async function's <c>Promise&lt;T&gt;</c> param
/// receives a passed-in <c>$Promise</c>; only the enclosing function's <i>return</i> builds a real
/// Task.
/// </summary>
private static bool IsPromiseTaskSlot(Type mapped) =>
mapped == typeof(Task) ||
(mapped.IsGenericType && mapped.GetGenericTypeDefinition() == typeof(Task<>));

/// <summary>
/// True when <paramref name="type"/> is a union one of whose members is the <c>undefined</c>
/// type, so its runtime values include the $Undefined sentinel.
Expand Down
17 changes: 16 additions & 1 deletion Runtime/BuiltIns/ProcessBuiltIns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,22 @@ public static partial class ProcessBuiltIns
// Script start as a monotonic Stopwatch timestamp — reset each time a script
// begins execution. When set, uptime() reports time since script start rather
// than process start.
private static long? _scriptStartTimestamp;
//
// [ThreadStatic] because the in-process test runner shares this one CLR process
// across many concurrently-running scripts (xUnit parallelizes test collections):
// SetScriptArguments/ClearScriptArguments toggle this baseline (a recent timestamp
// ↔ null) on each run, and a *shared* field let one script's SetScriptArguments land
// between another script's two uptime() reads — the first read using the null →
// process-start baseline (large elapsed), the second the just-set recent baseline
// (near-zero elapsed) — so uptime() ran backwards and intermittently failed
// Process_Uptime_IncreasesOverTime (`up2 >= up1`). Per-thread isolation gives each
// script a stable baseline (same precedent as ThreadArgv/ThreadEnv above); the
// Stopwatch switch earlier fixed clock monotonicity but not this baseline race. In a
// real single-script process SetScriptArguments and the synchronous run share one
// thread, so script-relative uptime is preserved; an uptime() read on an async
// continuation thread falls back to the (process-start) baseline, differing only by
// the milliseconds between process and script start.
[ThreadStatic] private static long? _scriptStartTimestamp;

/// <summary>
/// Gets a member of the process object by name. Resolves process-specific
Expand Down
64 changes: 64 additions & 0 deletions SharpTS.Tests/CompilerTests/ILVerificationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,70 @@ async function main() {
Assert.Empty(errors);
}

[Fact]
public void FsFacadeImport_PassesILVerification()
{
// #1246: importing `fs` emitted 68 unverifiable methods across the facade. Two shapes,
// both a value stored into a narrower slot without the verifier-required bridge:
// (1) `object` → `string` — a $M_fs helper stages an `any`-typed argument (e.g.
// `__joinPath(src, names[i])` in cpSync) into a string parameter temp with no castclass;
// (2) `$Promise` → `Task<object>` — the promise primitives return a wrapped $Promise which
// the callback helpers (`__cbData`/`__cbVoid`) took in a `Promise<any>` → Task<object>
// parameter slot. Fixed by (1) a castclass on string parameter coercion and (2) widening
// non-async Promise<T> parameter slots to object (symmetric to #393's return-slot fix).
// Verifies the whole facade — every $M_fs_* method is emitted regardless of what is used.
var files = new Dictionary<string, string>
{
["main.ts"] = """
import * as fs from 'fs';
console.log(fs.statSync('.').isDirectory());
"""
};

var errors = TestHarness.CompileModulesAndVerifyOnly(files, "main.ts");

Assert.Empty(errors);
}

[Fact]
public void ObjectArgumentToStringParameter_PassesILVerification()
{
// #1246 class 1, minimal repro: an `any`-typed value (here a bracket read off an `any`
// array, which the emitter types as `object`) passed to a `string` parameter left the
// object on the stack where the callee's string slot was expected — no castclass. The
// JIT tolerated it; ILVerify reported StackUnexpected {Found object, Expected string}.
var source = """
function join(a: string, b: string): string { return a + "/" + b; }
const names: any = ["x", "y"];
console.log(join("d", names[0]));
""";

var (errors, output) = TestHarness.CompileVerifyAndRun(source);

Assert.Empty(errors);
Assert.Equal("d/x\n", output);
}

[Fact]
public void PromiseValueToPromiseParameter_PassesILVerification()
{
// #1246 class 2, minimal repro: a runtime $Promise (from Promise.resolve / new Promise —
// not a real CLR Task, unlike an async function's result) passed to a `Promise<any>`
// parameter, whose slot mapped to Task<object>. Storing the $Promise into the Task<object>
// slot ran fine but failed ILVerify {Found $Promise, Expected Task<object>}. The parameter
// slot is now widened to object.
var source = """
function consume(p: Promise<any>, cb: any): void { p.then((v: any) => cb(v)); }
consume(Promise.resolve(5), (v: any) => console.log(v));
consume(new Promise<any>((res: any) => res(7)), (v: any) => console.log(v));
""";

var (errors, output) = TestHarness.CompileVerifyAndRun(source);

Assert.Empty(errors);
Assert.Equal("5\n7\n", output);
}

[Fact]
public void ClassGetPropertyWithTypedGetter_PassesILVerification()
{
Expand Down
Loading