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
128 changes: 128 additions & 0 deletions Compilation/ArrowBoxedAdapterEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Reflection;
using System.Reflection.Emit;

namespace SharpTS.Compilation;

/// <summary>
/// Emits per-arrow "boxed adapter" static methods that let an arrow with
/// <b>annotated</b> parameters be dispatched by the array HOF direct-delegate
/// fast path (#861).
/// </summary>
/// <remarks>
/// <para>
/// An annotated callback like <c>(x: number): number =&gt; x*2</c> compiles to a
/// static method with the typed CLR signature <c>double(double)</c>, which cannot
/// bind to the <c>Func&lt;object,object&gt;</c> that the emitted
/// <c>Array*Direct</c> helpers expect. Without a bridge, such callbacks fall back
/// to the reflective <c>$TSFunction</c>/<c>MethodInvoker</c> path (per-element
/// dispatch + an <c>object[]</c> allocation), which is exactly the cost #861
/// targets.
/// </para>
/// <para>
/// The adapter is a static method <c>object Adapter(object[, object])</c> on
/// <c>$Program</c> whose body unboxes/casts each boxed element into the arrow's
/// typed parameter slot, <c>call</c>s the typed arrow method, then reboxes the
/// result. It binds to <c>Func&lt;object,object&gt;</c> /
/// <c>Func&lt;object,object,object&gt;</c> via <c>ldnull</c>+<c>ldftn</c>, so the
/// unchanged <c>Array*Direct</c> helpers drive it with a direct delegate call.
/// </para>
/// <para>
/// The unbox/box marshalling is shared with <see cref="DelegateAdapterEmitter"/>
/// (<see cref="DelegateAdapterEmitter.EmitUnboxForReturn"/> coerces
/// <c>object</c>→typed slot; <see cref="DelegateAdapterEmitter.EmitBoxForTS"/>
/// reboxes the typed result), so the conversion matches the reflective
/// <c>MethodInvoker</c> semantics for the no-arg-conversion regime (concrete
/// <c>double</c>/<c>bool</c>/<c>string</c> params — unions/nullable already widen
/// to <c>object</c> in <c>ParameterTypeResolver</c>, and the call site gates on
/// that). Only emits a static adapter, so it carries no <c>SharpTS.dll</c>
/// reference (standalone-DLL constraint preserved).
/// </para>
/// </remarks>
internal sealed class ArrowBoxedAdapterEmitter
{
// Keyed by (typed arrow method, adapter arity). Arity is the delegate's
// parameter count (1 for map/filter/forEach/find/…, 2 for reduce); the arrow
// itself may declare fewer params, in which case the extra adapter args are
// ignored. A given arrow node is emitted by exactly one CompilationContext (the
// one containing its call site), so this per-context cache never double-defines;
// the adapter NAME is derived from the arrow's globally-unique method name so it
// stays collision-free across contexts that share the same $Program type.
private readonly Dictionary<(MethodBuilder, int, bool), MethodBuilder> _cache = [];

/// <summary>
/// Returns the boxed adapter for <paramref name="arrowMethod"/> bound to a delegate of
/// <paramref name="funcArity"/> object parameters, emitting it on <paramref name="carrierType"/>
/// on first request. <paramref name="instance"/> selects the carrier shape:
/// <list type="bullet">
/// <item>false (#861 L1): a STATIC adapter on <c>$Program</c> calling the non-capturing
/// static arrow (<c>&lt;&gt;Arrow_N</c>).</item>
/// <item>true (#861 L3): an INSTANCE adapter on the capturing arrow's display class, calling
/// <c>this.Invoke(...)</c>; the caller binds it to <c>(displayInstance, ldftn adapter)</c>.</item>
/// </list>
/// <paramref name="boolReturn"/> (#861 L4): when the arrow returns <c>bool</c> and a
/// <c>Func&lt;object,bool&gt;</c> predicate helper is used, the adapter returns the unboxed
/// <c>bool</c> directly (no rebox) so the <c>*DirectBool</c> helper skips the box + IsTruthy.
/// </summary>
public MethodBuilder GetOrEmit(TypeBuilder carrierType, MethodBuilder arrowMethod, int funcArity, bool instance, bool boolReturn)
{
var key = (arrowMethod, funcArity, boolReturn);
if (_cache.TryGetValue(key, out var existing)) return existing;

var objectType = typeof(object);
var adapterParams = new Type[funcArity];
for (int i = 0; i < funcArity; i++) adapterParams[i] = objectType;
var adapterReturnType = boolReturn ? typeof(bool) : objectType;

// Name keyed off the arrow method's name ($Program-unique <>Arrow_N for static; "Invoke"
// for instance, where the per-arrow display class scopes it) plus the return-shape marker.
// Assembly-visible.
var attrs = MethodAttributes.Assembly | (instance ? 0 : MethodAttributes.Static);
var adapter = carrierType.DefineMethod(
$"{arrowMethod.Name}${(boolReturn ? "bbox" : "box")}{funcArity}",
attrs,
adapterReturnType,
adapterParams);

var il = adapter.GetILGenerator();
var arrowParams = arrowMethod.GetParameters();
// For an instance adapter arg0 is `this` (pushed first as the Invoke receiver); the delegate
// args then start at arg1. A static adapter's delegate args start at arg0.
int argBase = instance ? 1 : 0;
if (instance)
il.Emit(OpCodes.Ldarg_0);

// Load only as many args as the arrow actually declares, coercing each boxed object into its
// typed parameter slot. A 0-/1-param arrow under a wider delegate ignores the surplus args.
for (int i = 0; i < arrowParams.Length; i++)
{
EmitLdarg(il, argBase + i);
DelegateAdapterEmitter.EmitUnboxForReturn(il, arrowParams[i].ParameterType);
}

il.Emit(instance ? OpCodes.Callvirt : OpCodes.Call, arrowMethod);

// bool-return: the arrow already returns bool (caller guarantees) — leave it unboxed for the
// Func<object,bool> contract. Otherwise rebox the typed result to object for Func<object,…>.
if (!boolReturn)
DelegateAdapterEmitter.EmitBoxForTS(il, arrowMethod.ReturnType);
il.Emit(OpCodes.Ret);

_cache[key] = adapter;
return adapter;
}

private static void EmitLdarg(ILGenerator il, int index)
{
switch (index)
{
case 0: il.Emit(OpCodes.Ldarg_0); break;
case 1: il.Emit(OpCodes.Ldarg_1); break;
case 2: il.Emit(OpCodes.Ldarg_2); break;
case 3: il.Emit(OpCodes.Ldarg_3); break;
default:
if (index <= 255) il.Emit(OpCodes.Ldarg_S, (byte)index);
else il.Emit(OpCodes.Ldarg, index);
break;
}
}
}
7 changes: 7 additions & 0 deletions Compilation/CompilationContext.Closures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public partial class CompilationContext
// Arrow function methods (arrow node -> method info)
public Dictionary<ArrowFunction, MethodBuilder> ArrowMethods { get; set; } = [];

// Boxed-callback adapters for annotated-param array HOF callbacks (#861).
// Lazily created (no constructor plumbing needed); emits per-arrow
// object(object[,object]) adapters on $Program so a typed arrow can bind to
// the Func<object,…> the Array*Direct helpers expect.
private ArrowBoxedAdapterEmitter? _arrowBoxedAdapters;
internal ArrowBoxedAdapterEmitter ArrowBoxedAdapters => _arrowBoxedAdapters ??= new ArrowBoxedAdapterEmitter();

// Module-scope const → literal-arrow bindings. Iterator-helper fast paths
// look up `Expr.Variable` callbacks here so `const sq = x => x*x; arr.map(sq)`
// gets the same direct-delegate dispatch as the inline-arrow form.
Expand Down
16 changes: 14 additions & 2 deletions Compilation/DelegateAdapterEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,13 @@ private MethodBuilder EmitInvoke(
/// TS <c>number</c>, <c>bool</c> stays boxed bool, reference types pass through.
/// Mirrors the interpreter's <c>DotNetMarshaller.WrapReturn</c> for the common primitives.
/// </summary>
private void EmitBoxForTS(ILGenerator il, Type paramType)
/// <remarks>
/// <c>internal static</c> so the boxed-callback adapter emitter for array HOFs
/// (<see cref="ArrowBoxedAdapterEmitter"/>, #861) shares the exact same
/// box/unbox conventions — keeping <c>number</c>↔boxed-<c>double</c> etc.
/// consistent across every emitted marshalling site.
/// </remarks>
internal static void EmitBoxForTS(ILGenerator il, Type paramType)
{
if (!paramType.IsValueType)
{
Expand Down Expand Up @@ -209,7 +215,13 @@ private void EmitBoxForTS(ILGenerator il, Type paramType)
/// delegate's declared return type. void: pop. object: no-op. value types: unbox via
/// boxed-double (matching <see cref="EmitBoxForTS"/>). Reference types: castclass.
/// </summary>
private void EmitUnboxForReturn(ILGenerator il, Type returnType)
/// <remarks>
/// <c>internal static</c>: also used by <see cref="ArrowBoxedAdapterEmitter"/>
/// (#861) to coerce a boxed array element into an arrow's typed parameter slot
/// (the inverse role), so the unbox/cast matches the reflective
/// <c>MethodInvoker</c> path exactly.
/// </remarks>
internal static void EmitUnboxForReturn(ILGenerator il, Type returnType)
{
if (returnType == typeof(void))
{
Expand Down
Loading
Loading