From 458b9b32f1be55f33cd4c4185282cec757c92469 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 15:56:08 +0100 Subject: [PATCH] An object's closure field keeps its captures through an interface `interface HasFn { fn: () => int }` with `const o = { fn: cl }`, `cl` a capturing closure: `const h: HasFn = o; h.fn()` printed garbage. The literal stores the closure as a bound function (its `this` is the capture tuple), the interface slot is read as a function taking the object as its `this`. The cast cloned the object with the interface's field type, and bound_func -> func dropped the captures, so the call bound the object where the captures belonged. A hybrid field (a type-literal annotated one) crashed the same way once it captured, an interface method (`fn(): int`) did too, and a plain function (`{ add: plusOne }`) received the object as its first argument. Such a field - bound, hybrid, or a plain function without a `this` parameter (MLIRTypeHelper::isObjectFieldFunctionWithoutThis) - now matches the interface's function-typed member as it is, and keeps its own type in a clone and in a literal built for the interface. The object's vtable calls it through a thunk, `..thunk(this, ...)`, that reads the field and calls it (generateSynthMethodToCallField, now taking the `this` type, shared with classes): a method's slot holds the thunk, a function-typed field's slot a tagged method cell holding it, as for a class since #612. As there, a write through the interface changes the shared cell. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 2 + .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 33 +++++ tslang/lib/TypeScript/LowerToLLVM.cpp | 3 +- tslang/lib/TypeScript/MLIRGenCast.cpp | 10 +- tslang/lib/TypeScript/MLIRGenClasses.cpp | 2 +- tslang/lib/TypeScript/MLIRGenImpl.h | 28 +++-- tslang/lib/TypeScript/MLIRGenInterfaces.cpp | 113 ++++++++++++++++- tslang/test/tester/CMakeLists.txt | 3 + ...ace_function_typed_field_object_closure.ts | 119 ++++++++++++++++++ 9 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 tslang/test/tester/tests/00interface_function_typed_field_object_closure.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index ae9c3f86e..55f9018dd 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -166,6 +166,8 @@ // pointer-sized field (or of optional, which starts with one) is pointer-aligned, and so // is the cell; -1 (absent) is tested before it. A write through such a field changes the cell, // so it changes the method for every instance of the class seen through that interface. +// A class or object field holding a function the interface can't call with the object as its +// `this` (a closure) is filled the same way, with a thunk calling the field. #define INTERFACE_METHOD_CELL_TAG 1 // on an InterfaceSymbolRefOp of a `v?: T` field that is stored to: true sets the flag of a tagged slot's // field, false (a store of undefined) clears it diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index 72755b81d..df221a58b 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -1141,6 +1141,26 @@ class MLIRTypeHelper return isa(inFuncType) || isa(inFuncType); } + // An object's field holding a function an interface slot can't call with the object as its `this`: + // a bound function (a capturing closure, its `this` is the captures), a hybrid one (a type-literal + // annotated field) or a plain function without a `this` parameter (`{ add: plusOne }`). Only an + // object-literal method (`fn() {...}`, `fn: () => ...` written in the literal) takes the object. + static bool isObjectFieldFunctionWithoutThis(mlir::Type fieldType) + { + if (isa(fieldType) || isa(fieldType)) + { + return true; + } + + if (auto funcType = dyn_cast(fieldType)) + { + return funcType.getNumInputs() == 0 + || !isa(funcType.getInput(0)) && !isa(funcType.getInput(0)); + } + + return false; + } + MatchResult TestFunctionTypesMatchWithObjectMethods(mlir::Location location, mlir::Type inFuncType, mlir::Type resFuncType, unsigned startParamIn = 0, unsigned startParamRes = 0) { @@ -1424,6 +1444,19 @@ class MLIRTypeHelper if (foundIndex >= 0) { auto foundField = tupleStorageType.getFieldInfo(foundIndex); + + // a function the slot can't hand out as one taking the object as its `this` is called + // through a thunk reading the field (isObjectFieldFunctionWithoutThis): the slot has the + // interface's type + if (isa(fieldType) && fieldType != foundField.type + && isObjectFieldFunctionWithoutThis(foundField.type)) + { + if (TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType).result == MatchResultType::Match) + { + return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; + } + } + auto test = isa(foundField.type) && isa(fieldType) ? TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType).result == MatchResultType::Match : stripLiteralType(fieldType) == stripLiteralType(foundField.type); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 8ec50fb75..a914e55c5 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6476,7 +6476,8 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(interfaceSymbolRefOp.getType()); auto isFunctionField = fieldRefType && isa(fieldRefType.getElementType()); diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index bab72652b..56dad6aa7 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -21,7 +21,10 @@ namespace mlirgen // member order (methods-first, per InterfaceInfo::getTupleTypeFields) // silently reads/writes the wrong slots at runtime. Only the TYPES are taken // from the interface; interface-only members are appended after the source - // fields. + // fields. A function the interface can't call with the object as its `this` + // (a closure, see MLIRTypeHelper::isObjectFieldFunctionWithoutThis) keeps its + // own type: cast to the interface's it would lose its captures. Its slot calls a + // thunk reading the field instead (mlirGenObjectVirtualTableDefinitionForInterface). static mlir::LogicalResult getInterfaceCloneFields(mlir::ArrayRef srcFields, InterfaceInfo::TypePtr interfaceInfo, mlir::MLIRContext *context, SmallVector &fields) @@ -37,7 +40,10 @@ namespace mlirgen auto interfaceField = std::find_if(interfaceFields.begin(), interfaceFields.end(), [&](auto &item) { return item.id == origField.id; }); - fields.push_back(interfaceField != interfaceFields.end() ? *interfaceField : origField); + auto keepsOwnType = interfaceField != interfaceFields.end() + && isa(interfaceField->type) + && MLIRTypeHelper::isObjectFieldFunctionWithoutThis(origField.type); + fields.push_back(interfaceField != interfaceFields.end() && !keepsOwnType ? *interfaceField : origField); } for (auto &interfaceField : interfaceFields) diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index c855b543e..c647af7a6 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2415,7 +2415,7 @@ genContext); auto funcType = mlir::cast(fieldType); auto thunkName = concat(interfaceVTableNameForClass(newClassPtr, newInterfacePtr), fieldName.getValue(), "thunk"); - if (mlir::failed(generateSynthMethodToCallField(location, newClassPtr, fieldName.getValue(), + if (mlir::failed(generateSynthMethodToCallField(location, newClassPtr->classType, fieldName.getValue(), thunkName, funcType, genContext))) { return {emptyFieldInfo, mlir::failure()}; diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 1fcb3b174..8e2494992 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -10670,6 +10670,19 @@ class MLIRGenImpl type = mth.wideStorageType(type); + // the receiver's slot takes the object as the function's `this` (an interface's function-typed member), + // the value does not - a closure, whose `this` is its captures (MLIRTypeHelper::isObjectFieldFunctionWithoutThis): + // cast, it would lose them. It keeps its own type, and the object's cast to the interface calls it through a + // thunk (mlirGenObjectVirtualTableDefinitionForInterface) + if (auto receiverFuncType = dyn_cast_or_null(receiverElementType)) + { + if (!mth.isObjectFieldFunctionWithoutThis(receiverFuncType) && mth.isObjectFieldFunctionWithoutThis(type) + && mth.TestFunctionTypesMatchWithObjectMethods(location, type, receiverFuncType).result == MatchResultType::Match) + { + receiverElementType = mlir::Type(); + } + } + if (receiverElementType) { if (type != receiverElementType) @@ -12157,9 +12170,10 @@ class MLIRGenImpl } // `(this, p0..pn) { return this.(p0..pn); }`, typed as the interface's - // function-typed field (`this` opaque): the method an interface slot calls for a class whose - // field of that name holds the function (see mlirGenClassVirtualTableDefinitionForInterface) - mlir::LogicalResult generateSynthMethodToCallField(mlir::Location location, ClassInfo::TypePtr newClassPtr, + // function-typed field (`this` opaque): the method an interface slot calls for a class or an + // object (`thisType`) whose field of that name holds the function (see + // mlirGenClassVirtualTableDefinitionForInterface, mlirGenObjectVirtualTableDefinitionForInterface) + mlir::LogicalResult generateSynthMethodToCallField(mlir::Location location, mlir::Type thisType, StringRef fieldName, StringRef thunkName, mlir_ts::FunctionType funcType, const GenContext &genContext) { @@ -12170,8 +12184,8 @@ class MLIRGenImpl GenContext funcGenContext(genContext); funcGenContext.clearScopeVars(); - funcGenContext.thisType = newClassPtr->classType; - funcGenContext.thisClassType = newClassPtr->classType; + funcGenContext.thisType = thisType; + funcGenContext.thisClassType = dyn_cast(thisType); funcGenContext.disableSpreadParams = true; return mlirGenFunctionBody( @@ -12180,8 +12194,8 @@ class MLIRGenImpl // the interface passes the object as an opaque `this` auto funcOp = genContext.funcOp; auto opaqueThis = funcOp.getBody().front().getArgument(0); - auto thisValue = builder.create(location, newClassPtr->classType, opaqueThis); - auto thisDecl = std::make_shared(THIS_NAME, newClassPtr->classType, location); + auto thisValue = builder.create(location, thisType, opaqueThis); + auto thisDecl = std::make_shared(THIS_NAME, thisType, location); DECLARE(thisDecl, thisValue); NodeFactory nf(NodeFactoryFlags::None); diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index cf1ca3c70..a46ad5d86 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -254,8 +254,10 @@ namespace mlirgen return mlir::failure(); } + // a field the interface calls through a thunk has the thunk in its slot already auto fieldInfo = mth.getFieldInfoByIndex(storeType, index); - if (lookupObjectLiteralMethodSymbol(fieldInfo.type, fieldId).empty()) + if (lookupObjectLiteralMethodSymbol(fieldInfo.type, fieldId).empty() + && !mth.isObjectFieldFunctionWithoutThis(fieldInfo.type)) { methodsNeedingPatch.push_back({methodPtr, vtableOffset}); } @@ -500,8 +502,80 @@ namespace mlirgen virtualTable.insert(virtualTable.begin(), VirtualMethodOrFieldInfo(noInstanceOf)); - // register global auto fullClassInterfaceVTableFieldName = interfaceVTableNameForObject(objectType, newInterfacePtr); + + // a field holding a function the interface can't call with the object as its `this` - a closure, whose + // `this` is its captures (MLIRTypeHelper::isObjectFieldFunctionWithoutThis) - is called through a thunk + // reading the field, `..thunk(this, ...) { return this.(...) }`. A method's slot holds + // the thunk; a function-typed field's slot the address of a cell holding it, tagged with + // INTERFACE_METHOD_CELL_TAG, as for a class (mlirGenClassVirtualTableDefinitionForInterface) + auto methodCellName = [&](mlir::Attribute id) { + return concat(fullClassInterfaceVTableFieldName, mlir::cast(id).getValue(), "method"); + }; + + llvm::SmallVector> thunksForFields; + for (auto &methodOrField : virtualTable) + { + if (!methodOrField.isField || methodOrField.isMissing) + { + continue; + } + + auto id = methodOrField.fieldInfo.id; + auto funcType = dyn_cast(methodOrField.fieldInfo.type); + auto fieldName = dyn_cast(id); + auto storageIndex = tupleStorageType.getIndex(id); + if (!funcType || !fieldName || storageIndex < 0) + { + continue; + } + + auto storageFieldType = tupleStorageType.getFieldInfo(storageIndex).type; + if (storageFieldType == funcType || !mth.isObjectFieldFunctionWithoutThis(storageFieldType)) + { + continue; + } + + // a field reached twice through a diamond `extends` has two slots and one thunk + if (llvm::any_of(thunksForFields, [&](auto &thunkForField) { return thunkForField.first == id; })) + { + continue; + } + + auto thunkName = concat(fullClassInterfaceVTableFieldName, fieldName.getValue(), "thunk"); + if (mlir::failed(generateSynthMethodToCallField(location, objectType, fieldName.getValue(), thunkName, + funcType, genContext))) + { + return mlir::failure(); + } + + thunksForFields.push_back({id, thunkName}); + + if (newInterfacePtr->findMethod(fieldName.getValue())) + { + continue; + } + + auto cellType = registerVariable( + location, methodCellName(id), true, VariableType::Var, + [&](mlir::Location location, const GenContext &genContext) { + auto thunkValue = builder.create( + location, funcType, mlir::FlatSymbolRefAttr::get(builder.getContext(), thunkName)); + return TypeValueInitType{funcType, thunkValue, TypeProvided::Yes}; + }, + genContext); + if (!cellType) + { + return mlir::failure(); + } + } + + auto thunkFor = [&](mlir::Attribute id) -> StringRef { + auto found = llvm::find_if(thunksForFields, [&](auto &thunkForField) { return thunkForField.first == id; }); + return found != thunksForFields.end() ? found->second : StringRef(); + }; + + // register global registerVariable( location, fullClassInterfaceVTableFieldName, true, VariableType::Var, [&](mlir::Location location, const GenContext &genContext) { @@ -540,16 +614,45 @@ namespace mlirgen // PropertySignature-with-function-type's slot corrupts that // computation (crashes on the resulting garbage address). std::string methodFuncName; + auto thunkName = methodOrField.isMissing ? StringRef() : thunkFor(methodOrField.fieldInfo.id); + auto isMethod = false; if (!methodOrField.isMissing) { auto fieldNameAttr = dyn_cast(methodOrField.fieldInfo.id); - if (fieldNameAttr && newInterfacePtr->findMethod(fieldNameAttr.getValue())) + isMethod = fieldNameAttr && newInterfacePtr->findMethod(fieldNameAttr.getValue()); + if (isMethod) { - methodFuncName = lookupObjectLiteralMethodSymbol(methodOrField.fieldInfo.type, methodOrField.fieldInfo.id); + methodFuncName = !thunkName.empty() + ? thunkName.str() + : lookupObjectLiteralMethodSymbol(methodOrField.fieldInfo.type, methodOrField.fieldInfo.id); } } - if (!methodFuncName.empty()) + if (!thunkName.empty() && !isMethod) + { + // the slot is the address of the thunk's cell plus INTERFACE_METHOD_CELL_TAG, so an access + // reads the cell instead of `this` + offset (InterfaceSymbolRefOpLowering) + auto cellRef = resolveFullNameIdentifier(location, methodCellName(methodOrField.fieldInfo.id), true, genContext); + if (!cellRef) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::Yes}; + } + + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto cellAddress = builder.create(location, ptrIntType, cellRef); + auto tagValue = builder.create( + location, ptrIntType, builder.getIntegerAttr(ptrIntType, INTERFACE_METHOD_CELL_TAG)); + auto taggedValue = builder.create( + location, ptrIntType, builder.getI32IntegerAttr((int)SyntaxKind::PlusToken), cellAddress, + tagValue); + auto slotValue = builder.create( + location, mlir_ts::RefType::get(methodOrField.fieldInfo.type), taggedValue); + + vtableValue = builder.create( + location, virtTuple, slotValue, vtableValue, + MLIRHelper::getStructIndex(builder, fieldIndex)); + } + else if (!methodFuncName.empty()) { auto methodConstName = builder.create( location, mlir::cast(methodOrField.fieldInfo.type), diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c90517699..62f6b8534 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -613,6 +613,7 @@ tslang_add_test(NAME test-compile-00-interface-optional-cast-order COMMAND test- tslang_add_test(NAME test-compile-00-interface-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") tslang_add_test(NAME test-compile-00-interface-function-typed-field-class COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class.ts") tslang_add_test(NAME test-compile-00-interface-function-typed-field-class-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class_field.ts") +tslang_add_test(NAME test-compile-00-interface-function-typed-field-object-closure COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_object_closure.ts") tslang_add_test(NAME test-compile-00-interface-optional-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field.ts") tslang_add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") tslang_add_test(NAME test-compile-00-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") @@ -1169,6 +1170,7 @@ tslang_add_test(NAME test-jit-00-interface-optional-cast-order COMMAND test-runn tslang_add_test(NAME test-jit-00-interface-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field.ts") tslang_add_test(NAME test-jit-00-interface-function-typed-field-class COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class.ts") tslang_add_test(NAME test-jit-00-interface-function-typed-field-class-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_class_field.ts") +tslang_add_test(NAME test-jit-00-interface-function-typed-field-object-closure COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_typed_field_object_closure.ts") tslang_add_test(NAME test-jit-00-interface-optional-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field.ts") tslang_add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") tslang_add_test(NAME test-jit-00-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") @@ -2033,6 +2035,7 @@ set(TSLANG_CORPUS 00interface_function_typed_field.ts 00interface_function_typed_field_class.ts 00interface_function_typed_field_class_field.ts + 00interface_function_typed_field_object_closure.ts 00interface_generic.ts 00interface_global_method.ts 00interface_indexer.ts diff --git a/tslang/test/tester/tests/00interface_function_typed_field_object_closure.ts b/tslang/test/tester/tests/00interface_function_typed_field_object_closure.ts new file mode 100644 index 000000000..18ff79f96 --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_typed_field_object_closure.ts @@ -0,0 +1,119 @@ +// regression test: an object literal's field holding a function that does not take +// the object as its `this` - a capturing closure (bound function), a hybrid function +// (a type-literal annotated field) or a plain global function - fills an interface +// member declared as a function-typed field (`fn: () => int`) or as a method +// (`fn(): int`). The interface calls its slot with the object as `this`, so the +// closure's captured context used to be dropped (garbage, or a crash) and a global +// function received the object as its first argument. The slot now calls a thunk +// that reads the field from the object and calls it. + +interface HasFn { + fn: () => int; +} + +interface HasMethod { + fn(): int; +} + +interface Adder { + add: (a: int) => int; +} + +interface Counted { + n: number; + fn: () => int; +} + +interface MaybeFn { + fn?: () => int; +} + +interface Pair { + first: (a: int) => int; + second(a: int, b: int): int; +} + +function plusOne(a: int): int { + return a + 1; +} + +function times(a: int, b: int): int { + return a * b; +} + +// the literal is built for the interface: the closure keeps its own type in it +function make(n: int): HasFn { + const cl = () => n * 3; + return { fn: cl }; +} + +function main() { + let k = 5; + const cl = () => k + 1; + + // a capturing closure filling a function-typed field + const o = { fn: cl }; + const h: HasFn = o; + print(h.fn()); + assert(h.fn() == 6); + + // the closure reads the captured variable, not a copy of it + k = 10; + assert(h.fn() == 11); + + // ... and filling a method + const m: HasMethod = o; + assert(m.fn() == 11); + + // a closure taking a parameter + let base = 100; + const o2 = { add: (a: int) => a + base }; + const add = o2.add; + const o3 = { add: add }; + const a1: Adder = o3; + assert(a1.add(1) == 101); + + // a type-literal annotated field holds a hybrid function + const o4: { add: (a: int) => int } = { add: (a: int) => a + k }; + const a2: Adder = o4; + assert(a2.add(1) == 11); + + // a plain function taking parameters gets them, not the object + const o5 = { add: plusOne }; + const a3: Adder = o5; + assert(a3.add(10) == 11); + + const o6 = { first: plusOne, second: times }; + const p: Pair = o6; + assert(p.first(1) == 2); + assert(p.second(3, 4) == 12); + + // an s32 field read as `number` forces a clone of the object: the closure survives it + const o7 = { n: 7, fn: cl }; + const c: Counted = o7; + assert(c.n == 7); + assert(c.fn() == 11); + + // a literal typed by the interface it is assigned to + const h9: HasFn = { fn: cl }; + assert(h9.fn() == 11); + + // the closure outlives the function that made it + const made = make(4); + assert(made.fn() == 12); + const arr: HasFn[] = [make(1), make(2)]; + let sum = 0; + for (const x of arr) sum += x.fn(); + assert(sum == 9); + + // an optional function-typed field + const mf: MaybeFn = o; + assert(mf.fn?.() == 11); + + // an object literal method still sees its object + const o8 = { v: 3, fn() { return this.v * 2; } }; + const h8: HasFn = o8; + assert(h8.fn() == 6); + + print("done."); +}