Three independent mechanisms classify every method call identically:
Path 1 — Compiler : typeNeedsMetaABIAlloca() (ASTContext.cpp)
Path 2 — C macro : mulle_metaabi_is_voidptr_compatible_expression()
Path 3 — Encoding : mulle_objc_signature_get_metaabiparamtype()
Decision Tree #
Return and param are classified independently, then combined. A struct/FP return forces STRUCT mode because the buffer must hold the return slot.
┌──────────────┐
│ RETURN TYPE │
└──────┬───────┘
│
┌──────────────────┼──────────────────────┐
│ │ │
┌─────▼──────┐ ┌──────▼──────────┐ ┌───────▼──────────┐
│ void │ │ void*-compat │ │ float / double / │
│ │ │ (id, int, ptr, │ │ long double / │
│ │ │ SEL, bool, │ │ struct / union │
│ │ │ Class, ^block) │ │ (ANY size) │
└─────┬──────┘ └──────┬──────────┘ └───────┬──────────┘
│ │ │
│ │ ═══► STRUCT ═══
│ │ rval in buffer,
│ │ all params in buffer too.
│ │ No further branching needed.
│ │
▼ ▼
┌────────────┐ ┌────────────┐
│ # PARAMS │ │ # PARAMS │
└──┬───┬───┬─┘ └──┬───┬───┬─┘
│ │ │ │ │ │
0 1 2+ 0 1 2+
│ │ │ │ │ │
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
VOID ① STR VOID ② STR
│ UC _PTR │ UC
│ T ③ │ T
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ PARAM TYPE │ │ PARAM TYPE │
└──┬──┬──┬──┬─┘ └──┬──┬──┬──┬─┘
│ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
A B C D E F G H
Leaf nodes — every real combination:
┌─────┬──────────────┬──────────────────┬──────────────────────────────────┐
│Leaf │ Return │ Param │ Call pattern │
├─────┼──────────────┼──────────────────┼──────────────────────────────────┤
│ │ │ │ │
│VOID │ void │ 0 params │ call(obj, sel, NULL) │
│ │ │ │ (discard return) │
│ │ │ │ │
│ A │ void │ 1 int/id/ptr/ │ call(obj, sel, (void*)val) │
│ │ │ SEL/bool │ (discard return) │
│ │ │ │ │
│ B │ void │ 1 float/double │ buf = {param}; │
│ │ │ │ call(obj, sel, &buf) │
│ │ │ │ (discard return) │
│ │ │ │ │
│ C │ void │ 1 tiny struct/ │ pack struct→void*; │
│ │ │ union ◄FIX► │ call(obj, sel, packed) │
│ │ │ │ (discard return) │
│ │ │ │ │
│ D │ void │ 1 large struct/ │ buf = {param}; │
│ │ │ union │ call(obj, sel, &buf) │
│ │ │ │ (discard return) │
│ │ │ │ │
│STRUC│ void │ 2+ params │ buf = {p1, p2, ...}; │
│ T │ │ │ call(obj, sel, &buf) │
│ │ │ │ (discard return) │
├─────┼──────────────┼──────────────────┼──────────────────────────────────┤
│ │ │ │ │
│VOID │ int/id/ptr/ │ 0 params │ rval = call(obj, sel, NULL) │
│_PTR │ SEL/bool │ │ result = (Type)rval │
│ ③ │ │ │ │
│ │ │ │ │
│ E │ int/id/ptr/ │ 1 int/id/ptr/ │ rval = call(obj, sel, (void*)val)│
│ │ SEL/bool │ SEL/bool │ result = (Type)rval │
│ │ │ │ │
│ F │ int/id/ptr/ │ 1 float/double │ buf = {param}; │
│ │ SEL/bool │ │ rval = call(obj, sel, &buf) │
│ │ │ │ result = (Type)rval │
│ │ │ │ │
│ G │ int/id/ptr/ │ 1 tiny struct/ │ pack struct→void*; │
│ │ SEL/bool │ union ◄FIX► │ rval = call(obj, sel, packed) │
│ │ │ │ result = (Type)rval │
│ │ │ │ │
│ H │ int/id/ptr/ │ 1 large struct/ │ buf = {param}; │
│ │ SEL/bool │ union │ rval = call(obj, sel, &buf) │
│ │ │ │ result = (Type)rval │
│ │ │ │ │
│STRUC│ int/id/ptr/ │ 2+ params │ buf = {p1, p2, ...}; │
│ T │ SEL/bool │ │ rval = call(obj, sel, &buf) │
│ │ │ │ result = (Type)rval │
├─────┼──────────────┼──────────────────┼──────────────────────────────────┤
│ │ │ │ │
│STRUC│ float/double │ any │ buf = {rval_slot, params...}; │
│ T │ │ │ call(obj, sel, &buf) │
│ │ │ │ result = *(FP_Type*)buf │
│ │ │ │ │
│STRUC│ struct/union │ any │ buf = {rval_slot, params...}; │
│ T │ (any size) │ │ call(obj, sel, &buf) │
│ │ │ │ result = *(StructType*)buf │
└─────┴──────────────┴──────────────────┴──────────────────────────────────┘
Note ③: -(id)self and -(int)count are VOID_PTR — _param is NULL but
the return value is in the register and must be captured. This is distinct
from VOID where the return is discarded.
1-param type classification (leaves A–H) #
┌──────────────────────────────────────────────────────────────────┐
│ Param type │ size ≤ void*? │ align ≤ void*? │ FP? │
│ │ │ │ │
│ int/id/ptr/SEL/bool │ yes │ yes │ no │
│ → VOID_PTR (A/E) │ │ │ │
│ │ │ │ │
│ float/double/long double│ yes (64-bit) │ yes │ YES │
│ → STRUCT (B/F) │ │ │ │
│ │ │ │ │
│ tiny struct/union │ yes │ yes │ n/a │
│ → VOID_PTR (C/G) │ ◄── OUR FIX: Path 3 now checks this │
│ │ │
│ large struct/union │ no │ maybe no │ n/a │
│ → STRUCT (D/H) │ │
└──────────────────────────────────────────────────────────────────┘
VOID_PTR — single void*-compatible param, non-struct/non-FP return #
PACK PARAM into void*:
┌──────────────────────────┬─────────────────────────────────────────┐
│ int / long / long long │ (void *)(intptr_t) value │
│ bool │ (void *)(intptr_t) value │
│ id / Class │ (void *) value │
│ ptr / ^block / SEL │ (void *) value │
│ tiny struct or union │ store into void*-sized tmp, reinterpret │
│ e.g. {float x, float y}│ alloca tmp; store struct; load tmp │
└──────────────────────────┴─────────────────────────────────────────┘
id result = call(obj, sel, packed);
UNPACK RETURN from result:
┌──────────────────────────┬─────────────────────────────────────────┐
│ void │ (discard result) │
│ int / long / long long │ (Type)(intptr_t) result │
│ bool │ result != NULL │
│ id / Class / ptr / SEL │ (Type) result │
└──────────────────────────┴─────────────────────────────────────────┘
Examples:
-(void) setName:(id)name— leaf A: pack id, discard return-(void) move:(Point){ff}p— leaf C: pack tiny-struct, discard return ◄ FIX-(id) objectAtIndex:(int)i— leaf E: pack int, return id-(id) pointAsObject:(Point)p— leaf G: pack tiny-struct, return id-(NSUInteger)count— leaf ③: no param, return int
STRUCT — multi-param, FP param, large struct, or any struct/FP return #
The metaabi buffer layout depends on whether the return type needs a slot:
If return type is void, int, id, ptr (no struct-return slot):
_param ──► ┌─────────────────────────────┐
│ param1 (aligned) │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ param2 (aligned) │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ ... │
└─────────────────────────────┘
call(obj, sel, (id)_param);
rval via normal return register (id/void*)
If return type is float, double, struct, or union (struct-return slot at front):
_param ──► ┌─────────────────────────────┐
│ return value slot │ ◄── callee writes here
│ (sizeof ReturnType bytes) │ ◄── caller reads after call
├─────────────────────────────┤
│ param1 (aligned) │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ param2 (aligned) │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ ... │
└─────────────────────────────┘
call(obj, sel, (id)_param);
ReturnType result = *(ReturnType *)_param; // read from front
Note: all params and the return slot are naturally aligned within the buffer.
Example Matrix #
Encoding Return Params Mode Leaf Notes
───────────────────────────────────────────────────────────────────────────────
-(void) reset void 0 VOID
-(void) setName:(id)s void 1 id VOID_PTR A
-(void) setCount:(int)n void 1 int VOID_PTR A
-(void) setFlag:(BOOL)b void 1 bool VOID_PTR A
-(void) setFloat:(float)f void 1 float STRUCT B FP param
-(void) setDouble:(double)d void 1 double STRUCT B FP param
-(void) move:(Point)p {ff} 8B void 1 tiny-struct VOID_PTR C ◄ FIXED
-(void) setRect:(Rect)r {ffff} void 1 large-struc STRUCT D >8 bytes
-(void) setXY:(int)x y:(int)y void 2 int+int STRUCT 2 params
-(void) foo:(float)f b:(float)g void 2 float+float STRUCT 2 params
-(void) foo:(Point)a b:(Point)b void 2 tiny+tiny STRUCT 2 params
───────────────────────────────────────────────────────────────────────────────
-(id) self id 0 VOID_PTR ③ no param
-(id) objectAt:(int)i id 1 int VOID_PTR E
-(id) wrap:(Point)p {ff} 8B id 1 tiny-struct VOID_PTR G ◄ FIXED
-(int) count int 0 VOID_PTR ③ no param
-(int) indexOf:(id)o int 1 id VOID_PTR E
-(int) add:(int)a b:(int)b int 2 int+int STRUCT 2 params
───────────────────────────────────────────────────────────────────────────────
-(float) val float 0 STRUCT FP return
-(float) dot:(Point)p float 1 tiny-struct STRUCT FP return
-(float) dot:(Point)a b:(Point)b float 2 tiny+tiny STRUCT FP return
-(double) area:(Rect)r double 1 large-struc STRUCT FP return
───────────────────────────────────────────────────────────────────────────────
-(Point) origin {ff} 8B tiny-s 0 STRUCT struct rval
-(Point) scaled:(float)f tiny-s 1 float STRUCT struct rval
-(Point) add:(Point)a b:(Point)b tiny-s 2 tiny+tiny STRUCT struct rval
-(CGRect) bounds {ffff} large-s 0 STRUCT struct rval
-(NSRange) range:(int)loc {QQ} 16B large-s 1 int STRUCT struct rval
───────────────────────────────────────────────────────────────────────────────
Key insight: the return type is tested first and can force STRUCT mode
regardless of the parameter types. A method like -(Point) mirror:(Point)p
goes STRUCT because the return type is a struct — not because of the param.
Why Return Structs Always Force STRUCT Mode #
Even a tiny struct return (sizeof == 4) goes through struct-buffer mode.
The caller allocates the buffer, passes a pointer in _param, and the callee
writes the return value into _param[0].
This asymmetry vs params (tiny struct param → VOID_PTR) exists because:
- Path 1 (compiler):
typeNeedsMetaABIAlloca(isParam=false)has no size check for struct/union — ALL struct/union returns unconditionally use alloca. - Path 2 (C macros):
mulle_metaabi_is_voidptr_compatible_expression()only checks the single parameter, not the return type. There is no equivalent_Generic-based check for the return type in the macro path. - Path 3 (encoding):
_mulle_metaabi_get_metaabiparamtype()returnsmulle_metaabi_param_structfor all struct/union types — the size-based override in_for_paramis only applied to params, not to return types. - All three paths must agree. Since Paths 2 and 3 have no mechanism to treat tiny struct returns differently, Path 1 keeps them consistent by not applying the size check on the return side either.
Why FP Params Force STRUCT Mode (even if ≤ sizeof(void*)) #
A float is 4 bytes, well under 8, but packing it into a void* via a
union type-pun causes a store-forwarding stall on x86:
; union pun — STALL (write 4 bytes FP, read 8 bytes GP from same addr)
movss %xmm0, (%rsp) ; 4-byte float store (SSE)
movq (%rsp), %rdi ; 8-byte GP load ← CPU stall ~5–20 cycles
; struct-buffer — no stall (just pass the address)
movss %xmm0, 8(%rsp) ; store float into alloca'd slot
leaq 8(%rsp), %rdi ; pass address — no memory reload at all
Benchmarked: ~35% faster at 100M iterations (-O2, 3 separate TUs).
Why Tiny Struct Params DO Use VOID_PTR (no FP poison) #
A struct with float members, e.g. {float x, float y}, does NOT trigger
the FP store-forwarding hazard because:
- The struct fields are already in memory (struct was in a local var/alloca).
- The memcpy/load sequence is GP→GP (no SSE register involved at the call site).
_Genericin the C macro path hitsdefault: 0for struct types, so the macro can't distinguish FP-member structs anyway.- Treating all tiny structs as void*-compatible is required for the C macro path (Path 2) to work without an ObjC compiler.
FP Return vs Tiny-Struct Return Asymmetry #
Type As param As return
──────────────────────────────────────
float STRUCT STRUCT
double STRUCT STRUCT
{ff} Point VOID_PTR ◄FIX STRUCT
{QQ} NSRange STRUCT STRUCT (large, >void*)
id VOID_PTR VOID_PTR
int VOID_PTR VOID_PTR
The fix makes {ff} Point as a param behave like int (VOID_PTR),
while leaving {ff} Point as a return value going through STRUCT —
exactly matching the compiler's typeNeedsMetaABIAlloca(isParam=true/false).