diff --git a/Compilation/ExpressionEmitterBase.CallHelpers.cs b/Compilation/ExpressionEmitterBase.CallHelpers.cs
index 53f9c2ea..6f9d4497 100644
--- a/Compilation/ExpressionEmitterBase.CallHelpers.cs
+++ b/Compilation/ExpressionEmitterBase.CallHelpers.cs
@@ -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);
}
}
diff --git a/Compilation/ILEmitter.Helpers.cs b/Compilation/ILEmitter.Helpers.cs
index 2a6018c7..d9657e61 100644
--- a/Compilation/ILEmitter.Helpers.cs
+++ b/Compilation/ILEmitter.Helpers.cs
@@ -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);
}
}
diff --git a/Compilation/ParameterTypeResolver.cs b/Compilation/ParameterTypeResolver.cs
index e5d0e3ae..7c321cf1 100644
--- a/Compilation/ParameterTypeResolver.cs
+++ b/Compilation/ParameterTypeResolver.cs
@@ -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;
}
+ ///
+ /// True when is a CLR Task/Task<T> produced by
+ /// strictly mapping a Promise<T> annotation. A parameter so typed receives the runtime
+ /// $Promise object at the call boundary, never a real CLR Task (the promise
+ /// primitives return $Promise, and no path constructs a bare Task guest value), so
+ /// the slot must widen to object. Mirrors the identical Promise<T> → object
+ /// widening already applied to non-async return slots in (#393);
+ /// its absence on parameters left the fs facade callback helpers emitting a $Promise
+ /// into a Task<object> slot, which the JIT tolerates but IL verification rejects
+ /// (#1246). The widening is unconditional — even an async function's Promise<T> param
+ /// receives a passed-in $Promise; only the enclosing function's return builds a real
+ /// Task.
+ ///
+ private static bool IsPromiseTaskSlot(Type mapped) =>
+ mapped == typeof(Task) ||
+ (mapped.IsGenericType && mapped.GetGenericTypeDefinition() == typeof(Task<>));
+
///
/// True when is a union one of whose members is the undefined
/// type, so its runtime values include the $Undefined sentinel.
diff --git a/Runtime/BuiltIns/ProcessBuiltIns.cs b/Runtime/BuiltIns/ProcessBuiltIns.cs
index 789415ad..dbdeffbf 100644
--- a/Runtime/BuiltIns/ProcessBuiltIns.cs
+++ b/Runtime/BuiltIns/ProcessBuiltIns.cs
@@ -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;
///
/// Gets a member of the process object by name. Resolves process-specific
diff --git a/SharpTS.Tests/CompilerTests/ILVerificationTests.cs b/SharpTS.Tests/CompilerTests/ILVerificationTests.cs
index 6142e32b..2e5727b4 100644
--- a/SharpTS.Tests/CompilerTests/ILVerificationTests.cs
+++ b/SharpTS.Tests/CompilerTests/ILVerificationTests.cs
@@ -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