diff --git a/src/ast_flat.h b/src/ast_flat.h index 5da5c85..50dc836 100644 --- a/src/ast_flat.h +++ b/src/ast_flat.h @@ -350,6 +350,14 @@ enum class TypeKind : uint8_t { // via the substitution engine when an LLVM type is requested or // when a binding's static TypeIdx is needed. GenericCall, + // Function-typed value: a pointer to a function with a known + // signature. Parsed from `fn(T1, T2) Ret` in type position. The + // TypeKey carries the return TypeIdx in `a` and an index into + // TypePool::fnParams in `b`. Lowered to LLVM `ptr` — the value + // itself is a code address; the signature is metadata the + // compiler consults at call sites to type-check arguments and + // pick the right LLVM function type for the indirect call. + Fn, }; struct TypeKey { @@ -395,6 +403,11 @@ inline bool operator==(const TypeKey &x, const TypeKey &y) { case TypeKind::Type: // Singleton meta-type: every TypeKey of kind Type is equal. return true; + case TypeKind::Fn: + // equal iff return type AND param-list index match. Params + // are interned in TypePool::fnParams_ so the index is + // canonical (two `fn(i32) i32` keys share the same b). + return x.a == y.a && x.b == y.b; case TypeKind::GenericCall: // equal iff callee name AND args-list index match. The // args index is canonical because the side table interns @@ -445,6 +458,13 @@ class TypePool { // once. std::vector> genericArgs_; std::map, uint32_t> genericArgsIdx_; + // Side table for function-type parameter lists. A `TypeKind::Fn` + // TypeKey stores the index into this vector in `b`. Two + // `fn(i32, str) i64` types interned at distinct sites resolve to + // the same TypeIdx because the param list `[i32, str]` is interned + // here once. + std::vector> fnParams_; + std::map, uint32_t> fnParamsIdx_; TypeIdx pushKey(TypeKey k) { TypeIdx id = static_cast(keys_.size()); @@ -527,6 +547,26 @@ class TypePool { const std::vector &genericArgsAt(uint32_t idx) const { return genericArgs_[idx]; } + + // intern a `fn(T1, T2) Ret` type. The param list is interned in + // fnParams_ so two identical signatures share an index and hence + // the same TypeIdx. Return type is stored directly in `a`. + TypeIdx internFn(TypeIdx returnTy, std::vector paramTys) { + uint32_t paramsIdx; + auto it = fnParamsIdx_.find(paramTys); + if (it != fnParamsIdx_.end()) { + paramsIdx = it->second; + } else { + paramsIdx = static_cast(fnParams_.size()); + fnParams_.push_back(paramTys); + fnParamsIdx_.emplace(std::move(paramTys), paramsIdx); + } + return intern(TypeKey{TypeKind::Fn, 0, 0, returnTy, paramsIdx}); + } + + const std::vector &fnParamsAt(uint32_t idx) const { + return fnParams_[idx]; + } TypeIdx internEnum(StringIdx nameId) { return intern(TypeKey{TypeKind::Enum, 0, 0, nameId, 0}); } diff --git a/src/astgen.cpp b/src/astgen.cpp index f7c4429..5eb826b 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -726,7 +726,8 @@ static void astgenVarDecl(AstGenCtx &gctx, const AstNode &n) { // initExpr;` at the module level and lower by re-evaluating the init // expression at each read site. Constant folding makes this cheap; // non-constant inits would require a runtime global slot (deferred). -static JirRef astgenVariable(AstGenCtx &gctx, const AstNode &n) { +static JirRef astgenVariable(AstGenCtx &gctx, const AstNode &n, + TypeIdx expected = kNoType) { const std::string &name = gctx.ctx.getStringPool().get(static_cast(n.lhs)); auto it = gctx.locals.find(name); @@ -741,11 +742,13 @@ static JirRef astgenVariable(AstGenCtx &gctx, const AstNode &n) { return astgenExpr(gctx, mc->initExpr, mc->declaredType); } // Fn-name-as-value (Rust-style item coercion). The identifier - // resolves to a function symbol — surface its address as a u64 - // so it can be assigned to a u64 slot, written to a buffer, or - // (with `as *mut[] u8`) round-tripped to a typed pointer. Generic - // fns are rejected because no monomorphized body exists at this - // point in lowering. + // resolves to a function symbol — surface its address as either + // (a) a typed function pointer when the context expects a Fn + // type, so `var f: fn(i32) i32 = add;` and struct fields of fn + // type get a properly-typed value, or (b) the legacy u64 when + // the context is untyped (writes to buffers, manual casts). + // Generic fns are rejected because no monomorphized body exists + // at this point in lowering. if (const FunctionAST *fn = gctx.ctx.getFunctionAST(name)) { if (fn->isGeneric()) { return recoverHere(gctx, @@ -757,7 +760,16 @@ static JirRef astgenVariable(AstGenCtx &gctx, const AstNode &n) { fnref.tag = JirTag::FnRef; fnref.a = static_cast( gctx.ctx.getStringPool().intern(fn->Name)); - fnref.ty = BuiltinType::U64; + // If the consumer asked for a Fn type, give them one; otherwise + // fall back to u64 (legacy raw-address shape). Future cleanup: + // always emit the typed Fn and let consumers cast to u64 + // explicitly via `as`. + bool expectFn = false; + if (expected != kNoType) { + const TypeKey &ek = gctx.ctx.getTypePool().get(expected); + expectFn = ek.kind == TypeKind::Fn; + } + fnref.ty = expectFn ? expected : BuiltinType::U64; return emit(gctx, fnref); } // Recoverable: emit a Poison so the rest of the function still @@ -3661,6 +3673,94 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n) { const FunctionAST *fn = gctx.ctx.getFunctionAST(callee); if (fn == nullptr) { + // Before erroring, try the fn-pointer-in-local-or-field paths. + // Two cases, both producing a Fn-typed JirRef we can call + // indirect through: + // (1) zero-dot callee `f` is a local whose type is Fn. + // `var f: fn(...) = ...; f(args);` + // (2) single-dot callee `recv.field` where `field` is a Fn- + // typed field on recv's struct. `w.writeFn(args);` + // Multi-dot callees (`x.y.z(args)`) take the indirect-call path + // in the parser already; this branch is only for the cases the + // parser emitted as direct Call (qualified-name based). + auto buildIndirectCall = [&](JirRef calleeVal) -> JirRef { + TypeIdx calleeTy = gctx.jfn.getInst(calleeVal).ty; + const TypeKey &k = gctx.ctx.getTypePool().get(calleeTy); + TypeIdx retTy = static_cast(k.a); + const auto ¶mTys = gctx.ctx.getTypePool().fnParamsAt(k.b); + std::vector argRefs; + argRefs.reserve(argCount); + for (uint32_t i = 0; i < argCount; i++) { + NodeIdx argIdx = + static_cast(ns.getExtra(argsExtra + 1 + i)); + TypeIdx expectArg = i < paramTys.size() ? paramTys[i] : kNoType; + argRefs.push_back(astgenExpr(gctx, argIdx, expectArg)); + } + std::vector packed; + packed.reserve(1 + argRefs.size()); + packed.push_back(static_cast(argRefs.size())); + for (JirRef r : argRefs) packed.push_back(static_cast(r)); + JirExtraIdx extraIdx = + gctx.jfn.pushExtra(packed.data(), packed.size()); + JirInst ic{}; + ic.tag = JirTag::CallIndirect; + ic.a = calleeVal; + ic.b = extraIdx; + ic.ty = retTy; + return emit(gctx, ic); + }; + + size_t dotPos = callee.find('.'); + if (dotPos == std::string::npos) { + // (1) bare name — is it a Fn-typed local? + auto it = gctx.locals.find(callee); + if (it != gctx.locals.end()) { + TypeIdx localTy = gctx.localTypes[callee]; + const TypeKey &k = gctx.ctx.getTypePool().get(localTy); + if (k.kind == TypeKind::Fn) { + JirInst load{}; + load.tag = JirTag::Load; + load.a = it->second; + load.ty = localTy; + JirRef fnVal = emit(gctx, load); + return buildIndirectCall(fnVal); + } + } + } else if (callee.find('.', dotPos + 1) == std::string::npos) { + // (2) single-dot `recv.field` — fall through only when + // `recv` is a local AND `field` is a Fn-typed field on + // its struct type. + std::string recvName = callee.substr(0, dotPos); + std::string fieldName = callee.substr(dotPos + 1); + auto it = gctx.locals.find(recvName); + if (it != gctx.locals.end()) { + TypeIdx recvTy = gctx.localTypes[recvName]; + const auto *sinfo = gctx.ctx.lookupStruct(recvTy); + if (sinfo != nullptr) { + for (size_t i = 0; i < sinfo->fields.size(); ++i) { + if (sinfo->fields[i].first != fieldName) continue; + TypeIdx fieldTy = sinfo->fields[i].second; + const TypeKey &fk = + gctx.ctx.getTypePool().get(fieldTy); + if (fk.kind != TypeKind::Fn) break; + // Load recv as a value, ExtractValue the field. + JirInst loadRecv{}; + loadRecv.tag = JirTag::Load; + loadRecv.a = it->second; + loadRecv.ty = recvTy; + JirRef recvVal = emit(gctx, loadRecv); + JirInst ev{}; + ev.tag = JirTag::ExtractValue; + ev.a = recvVal; + ev.b = static_cast(i); + ev.ty = fieldTy; + JirRef fnVal = emit(gctx, ev); + return buildIndirectCall(fnVal); + } + } + } + } + // Qualified callees (`lib.priv`) get the precise pub-access // diagnostic via formatNamespaceLookupError. std::string msg = @@ -3870,7 +3970,7 @@ static JirRef astgenExpr(AstGenCtx &gctx, NodeIdx node, TypeIdx expected, astgenVarDecl(gctx, n); return kNoJirRef; case AstTag::Variable: - result = astgenVariable(gctx, n); + result = astgenVariable(gctx, n, expected); break; case AstTag::Assign: astgenAssign(gctx, n); diff --git a/src/codegen.cpp b/src/codegen.cpp index ef90166..a140135 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -215,6 +215,17 @@ JamTypeRef JamCodegenContext::getLLVMType(TypeIdx ty) const { result = getLLVMType(resolved); break; } + case TypeKind::Fn: { + // Function-typed values lower to LLVM opaque pointers. The + // signature isn't part of the LLVM value type (LLVM 15+ uses + // opaque pointers everywhere); the Jam type system tracks the + // signature for call-site type-checking. The actual LLVM + // function type (return + params) is built per indirect call + // site by jir_codegen, reading the signature out of the + // TypeKey + fnParamsAt(k.b). + result = JamLLVMPointerType(getInt8Type(), 0); + break; + } } llvmTypeCache[ty] = result; return result; @@ -594,6 +605,9 @@ uint64_t JamCodegenContext::typeSize(TypeIdx ty) const { case TypeKind::GenericCall: // resolve and recurse. return typeSize(resolveGenericCall(ty)); + case TypeKind::Fn: + // Function value = code pointer = pointer width. + return 8; } throw std::runtime_error("typeSize: unhandled type kind"); } @@ -681,6 +695,9 @@ uint64_t JamCodegenContext::typeAlign(TypeIdx ty) const { return 1; case TypeKind::GenericCall: return typeAlign(resolveGenericCall(ty)); + case TypeKind::Fn: + // Function pointer alignment. + return 8; } throw std::runtime_error("typeAlign: unhandled type kind"); } @@ -728,6 +745,21 @@ TypeIdx substituteType(TypeIdx ty, return types.internGenericCall(static_cast(k.a), std::move(newArgs)); } + case TypeKind::Fn: { + // Function-typed value: substitute the return type AND every + // param type. A `fn(T) T` field inside a generic struct sees + // T → concrete on instantiation, so the field's TypeIdx must + // rebuild with substituted children. + TypeIdx retSub = + substituteType(static_cast(k.a), subst, types, strings); + const auto ¶ms = types.fnParamsAt(k.b); + std::vector newParams; + newParams.reserve(params.size()); + for (TypeIdx p : params) { + newParams.push_back(substituteType(p, subst, types, strings)); + } + return types.internFn(retSub, std::move(newParams)); + } default: return ty; } diff --git a/src/jam_llvm.cpp b/src/jam_llvm.cpp index 42084b9..964efdf 100644 --- a/src/jam_llvm.cpp +++ b/src/jam_llvm.cpp @@ -786,6 +786,26 @@ JamValueRef JamLLVMBuildCall(JamBuilderRef builder, JamFunctionRef func, argValues, name)); } +// Indirect call through a function pointer. The signature (return + +// param types) lives in `funcType` — built by the caller from the +// JIR-level Fn TypeIdx — and `callee` is the Value holding the code +// address (an opaque ptr under LLVM 15+). Used to lower JirTag:: +// CallIndirect, where the JIR's Fn type is the source of truth for +// the LLVM function-type the call instruction needs. +JamValueRef JamLLVMBuildIndirectCall(JamBuilderRef builder, + JamTypeRef funcType, JamValueRef callee, + JamValueRef *args, unsigned numArgs, + const char *name) { + std::vector argValues; + for (unsigned i = 0; i < numArgs; i++) { + argValues.push_back(UNWRAP_VALUE(args[i])); + } + llvm::FunctionType *ft = + llvm::cast(UNWRAP_TYPE(funcType)); + return WRAP_VALUE(UNWRAP_BUILDER(builder)->CreateCall(ft, UNWRAP_VALUE(callee), + argValues, name)); +} + JamValueRef JamLLVMBuildPhi(JamBuilderRef builder, JamTypeRef type, const char *name) { return WRAP_VALUE( diff --git a/src/jam_llvm.h b/src/jam_llvm.h index a8fe88d..1191d7c 100644 --- a/src/jam_llvm.h +++ b/src/jam_llvm.h @@ -321,6 +321,16 @@ JAM_EXTERN_C JamValueRef JamLLVMBuildCall(JamBuilderRef builder, JamFunctionRef func, JamValueRef *args, unsigned numArgs, const char *name); +// Indirect call through a function-typed value. `funcType` is the LLVM +// FunctionType built from the Jam-level Fn TypeIdx (return + params); +// `callee` is the ptr-typed Value holding the code address. Used by +// JirTag::CallIndirect lowering. +JAM_EXTERN_C JamValueRef JamLLVMBuildIndirectCall(JamBuilderRef builder, + JamTypeRef funcType, + JamValueRef callee, + JamValueRef *args, + unsigned numArgs, + const char *name); JAM_EXTERN_C JamValueRef JamLLVMBuildPhi(JamBuilderRef builder, JamTypeRef type, const char *name); JAM_EXTERN_C void JamLLVMAddIncoming(JamValueRef phi, JamValueRef *values, diff --git a/src/jir.h b/src/jir.h index 441a264..feda6b2 100644 --- a/src/jir.h +++ b/src/jir.h @@ -163,6 +163,15 @@ enum class JirTag : uint8_t { // `b` = ExtraIdx → [argCount, arg0, arg1, ...] // `ty` = return type (kNoType for void). Call, + // CallIndirect: `a` = JirRef of a fn-typed value (the function + // pointer); `b` = ExtraIdx → [argCount, arg0, ...]; + // `ty` = return type. The signature (return + params) + // is read from the JIR-level Fn TypeIdx on `a` and + // used by codegen to build the LLVM function type + // for the indirect call. Emitted whenever the callee + // isn't a known function name — fn-typed local, fn- + // typed struct field, fn-typed param, etc. + CallIndirect, // Function reference (Rust-style item-as-value). Resolves a fn // name to its address. `a` = StringIdx (function's LLVM symbol diff --git a/src/jir_codegen.cpp b/src/jir_codegen.cpp index 52ba24f..f276a27 100644 --- a/src/jir_codegen.cpp +++ b/src/jir_codegen.cpp @@ -373,10 +373,15 @@ static JamValueRef emitInstImpl(JirCodegenCtx &lctx, JirRef r) { return JamLLVMBuildIntToPtr(lctx.ctx.getBuilder(), v, ty, "i2p"); } case JirTag::FnRef: { - // `inst.a` carries the StringIdx of the LLVM symbol name. We - // resolve to the LLVM Function (already a ptr-typed Value), - // then lower to ptrtoint to match the JIR's u64 result type. - // This mirrors Rust's `my_fn as u64` lowering. + // `inst.a` carries the StringIdx of the LLVM symbol name. The + // LLVM Function is already a ptr-typed Value. Two result-type + // shapes: + // * `inst.ty` is `TypeKind::Fn` (typed function pointer) — + // return the function value directly (it's already a ptr; + // under LLVM 15+ opaque pointers this is the right shape + // for any callee-pointer use). + // * Otherwise (u64 raw-address shape) — ptrtoint to settle + // into an integer slot. Mirrors Rust's `my_fn as u64`. StringIdx nameId = static_cast(inst.a); const std::string &name = lctx.ctx.getStringPool().get(nameId); JamFunctionRef f = @@ -386,6 +391,13 @@ static JamValueRef emitInstImpl(JirCodegenCtx &lctx, JirRef r) { "`"); } JamValueRef fnVal = JamLLVMFunctionAsValue(f); + const TypeKey &dstKey = lctx.ctx.getTypePool().get(inst.ty); + if (dstKey.kind == TypeKind::Fn) { + // Already-ptr value; the JIR-level Fn type is the metadata + // the call-site uses to build the LLVM function type for + // the indirect call. + return fnVal; + } JamTypeRef ty = lctx.ctx.getLLVMType(inst.ty); return JamLLVMBuildPtrToInt(lctx.ctx.getBuilder(), fnVal, ty, "fnref.u64"); @@ -631,6 +643,53 @@ static JamValueRef emitInstImpl(JirCodegenCtx &lctx, JirRef r) { return JamLLVMBuildCall(lctx.ctx.getBuilder(), f, args.data(), static_cast(args.size()), resultName); } + case JirTag::CallIndirect: { + // Indirect call through a Fn-typed value. `inst.a` is the + // callee JirRef (its JIR type must be TypeKind::Fn); `inst.b` + // is the args extra slice. We build the LLVM FunctionType + // from the Fn signature on-demand — no sret/ABI tricks for + // now (those land when fn-pointer signatures need to match + // the same calling-convention machinery as direct calls). + JirRef calleeRef = static_cast(inst.a); + JamValueRef calleeVal = emitInst(lctx, calleeRef); + TypeIdx calleeTy = lctx.jfn.getInst(calleeRef).ty; + const TypeKey &k = lctx.ctx.getTypePool().get(calleeTy); + if (k.kind != TypeKind::Fn) { + throw std::runtime_error( + "jirCodegen: CallIndirect callee is not of Fn type"); + } + TypeIdx retTy = static_cast(k.a); + const auto ¶mTys = lctx.ctx.getTypePool().fnParamsAt(k.b); + + // Build LLVM function type for the call instruction. + std::vector llvmParamTys; + llvmParamTys.reserve(paramTys.size()); + for (TypeIdx pt : paramTys) { + llvmParamTys.push_back(lctx.ctx.getLLVMType(pt)); + } + JamTypeRef llvmRetTy = (retTy == kNoType) + ? lctx.ctx.getVoidType() + : lctx.ctx.getLLVMType(retTy); + JamTypeRef llvmFnTy = JamLLVMFunctionType( + llvmRetTy, llvmParamTys.data(), + static_cast(llvmParamTys.size()), /*isVarArgs=*/false); + + // Materialise argument values. + JirExtraIdx extra = static_cast(inst.b); + uint32_t argCount = lctx.jfn.getExtra(extra); + std::vector args; + args.reserve(argCount); + for (uint32_t i = 0; i < argCount; i++) { + JirRef ar = static_cast(lctx.jfn.getExtra(extra + 1 + i)); + args.push_back(emitInst(lctx, ar)); + } + + const char *resultName = (inst.ty == kNoType) ? "" : "call.indirect"; + return JamLLVMBuildIndirectCall(lctx.ctx.getBuilder(), llvmFnTy, + calleeVal, args.data(), + static_cast(args.size()), + resultName); + } // === Control === case JirTag::Br: { JirBlockRef target = static_cast(inst.a); diff --git a/src/jir_verify.cpp b/src/jir_verify.cpp index de3564a..4d8a1f2 100644 --- a/src/jir_verify.cpp +++ b/src/jir_verify.cpp @@ -151,6 +151,8 @@ const char *tagName(JirTag t) { return "Unreachable"; case JirTag::Call: return "Call"; + case JirTag::CallIndirect: + return "CallIndirect"; case JirTag::Param: return "Param"; case JirTag::StructLit: @@ -494,6 +496,22 @@ struct Verifier { } return; } + case JirTag::CallIndirect: { + // `a` is a JirRef of a Fn-typed value; `b` mirrors Call's + // extra-pool layout [argCount, arg0_ref, ...]. + checkRef(inst.a, false, r, "callee"); + if (inst.b >= jfn.extra.size()) { + err(r, "CallIndirect extra index out of bounds"); + return; + } + uint32_t argCount = jfn.extra[inst.b]; + checkExtraSlice(inst.b, 1 + argCount, r, "call-args"); + for (uint32_t i = 0; i < argCount; i++) { + JirRef ar = static_cast(jfn.extra[inst.b + 1 + i]); + checkRef(ar, false, r, "call-arg"); + } + return; + } case JirTag::StructLit: case JirTag::ArrayLit: { if (inst.b >= jfn.extra.size()) { diff --git a/src/parser.cpp b/src/parser.cpp index 8f42d5f..dc91b01 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -562,6 +562,23 @@ NodeIdx Parser::parsePrimary() { // [N]T — fixed-size array (mutability follows the binding; // `N` is part of the type) TypeIdx Parser::parseType() { + // `fn(T1, T2, ...) Ret` — function-typed value. Used for fields + // that hold a callback (Writer's writeFn, vtable-ish dispatch). + // The grammar is anchored on the `fn` keyword in type position to + // keep it unambiguous against bare identifiers. Return type is + // required (use `void` for callbacks that don't return a value). + if (match(TOK_FN)) { + consume(TOK_OPEN_PAREN, "Expected `(` after `fn` in type"); + std::vector paramTys; + if (!check(TOK_CLOSE_PAREN)) { + do { + paramTys.push_back(parseType()); + } while (match(TOK_COMMA)); + } + consume(TOK_CLOSE_PAREN, "Expected `)` after fn-type parameters"); + TypeIdx retTy = parseType(); + return typePool->internFn(retTy, std::move(paramTys)); + } if (match(TOK_STAR)) { bool ptrConst = false; if (match(TOK_CONST)) { diff --git a/tests/unit/test_fn_pointer.jam b/tests/unit/test_fn_pointer.jam new file mode 100644 index 0000000..8f59da3 --- /dev/null +++ b/tests/unit/test_fn_pointer.jam @@ -0,0 +1,57 @@ +const { assert } = import("test"); + +// Phase 0: fn-pointer types. Three shapes exercise the indirect- +// call path: +// 1. Fn-typed local → `var f: fn(...) = name; f(args);` +// 2. Fn-typed struct field → `s.field(args)` on a top-level struct +// 3. Fn-typed field via Self → `self.field(args)` inside a method +// on a generic struct (where Self resolves to the instantiation) + +fn add(a: i32, b: i32) i32 { return a + b; } +fn mul(a: i32, b: i32) i32 { return a * b; } + +fn callViaLocal() i32 { + var f: fn(i32, i32) i32 = add; + return f(3, 4); +} + +fn callViaLocalSwapped() i32 { + var f: fn(i32, i32) i32 = mul; + return f(3, 4); +} + +const Adder = struct { + ctx: i32, + op: fn(i32, i32) i32, +}; + +fn callViaStructField() i32 { + var a: Adder = Adder { ctx: 10, op: add }; + return a.op(a.ctx, 5); +} + +fn callViaStructFieldDifferentFn() i32 { + var a: Adder = Adder { ctx: 10, op: mul }; + return a.op(a.ctx, 5); +} + +fn Combiner(T: type) type { + return struct { + ctx: T, + op: fn(T, T) T, + fn run(self: Self, x: T) T { + return self.op(self.ctx, x); + } + }; +} + +fn callViaSelfFieldInGenericMethod() i32 { + var c: Combiner(i32) = Combiner(i32) { ctx: 100, op: add }; + return c.run(7); +} + +tfn fnPointerInLocal() { assert(callViaLocal(), 7); } +tfn fnPointerInLocalSwapped() { assert(callViaLocalSwapped(), 12); } +tfn fnPointerInStructField() { assert(callViaStructField(), 15); } +tfn fnPointerInStructFieldSwapped() { assert(callViaStructFieldDifferentFn(), 50); } +tfn fnPointerSelfFieldGenericMethod() { assert(callViaSelfFieldInGenericMethod(), 107); }