From 26b0dbaaed7c3347f63e1834512316b985ccefd1 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 18:08:55 +0000 Subject: [PATCH 1/3] Accessors fill abstract fields and interface fields A getter/setter implementing `abstract prop: T`, or an interface's `prop: T`, was read as a field: through the base a read printed null, and a class implementing an interface field with accessors did not compile (docs/not_impl/not_impl15.ts). - `abstract prop: T` has no storage now: it is an abstract get/set pair (getter only when readonly). A subclass fills it with accessors of its own, or with a field, which gets accessors over it. A plain field filling an abstract field used to be a second slot the base never read. - An interface field (not optional, not function-typed) a class fills with accessors has a slot holding the address of a {getter, setter} cell plus the sign bit (INTERFACE_ACCESSOR_CELL_TAG). Loads and stores through an interface field test the slot and call the accessor; RetainSlot/ReleaseSlot skip the cell, and a store releases the value the setter took. This adds a branch to every load and store of such an interface field. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 6 + tslang/lib/TypeScript/LowerToLLVM.cpp | 150 ++++++++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 302 +++++++++++++++++- tslang/lib/TypeScript/MLIRGenImpl.h | 8 + tslang/test/tester/CMakeLists.txt | 3 + .../00class_accessor_implements_field.ts | 112 +++++++ 6 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/tests/00class_accessor_implements_field.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 9d12e6fe5..f468bee38 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -220,6 +220,12 @@ // closure) is filled the same way, with a thunk calling the field; its cell is always present and // is not written through. #define INTERFACE_METHOD_CELL_TAG 1 +// A field (not optional, not function-typed) a class fills with accessors (`get prop()`, `set prop(v)`) has no +// storage in the object: its slot is instead the address of a global cell {getter, setter}, either null where +// the class has none, plus this bit - the sign bit, which an offset never has (a symbol plus a constant, so it +// is relocated as any address). A load through it calls the getter with `this`, and a store the setter +// (LoadOpLowering, StoreOpLowering). On a 32-bit target this holds for a cell below 2 GB. +#define INTERFACE_ACCESSOR_CELL_TAG(sizeBits) (1ULL << ((sizeBits) - 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 #define INTERFACE_OPTIONAL_WRITE_ATTR "__optional_write" diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index b930f60be..310c49ff1 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -558,6 +558,66 @@ class ReleaseContentsOpLowering : public TsLlvmPattern of(mlir::Value reference, ConversionPatternRewriter &rewriter, + const CompileOptions &compileOptions) + { + auto interfaceSymbolRefOp = reference.getDefiningOp(); + if (!interfaceSymbolRefOp || interfaceSymbolRefOp.getOptional().value_or(false) + || interfaceSymbolRefOp->hasAttr(INTERFACE_FUNCTION_FIELD_WRITE_ATTR) + || interfaceSymbolRefOp->hasAttr(INTERFACE_OPTIONAL_WRITE_ATTR)) + { + return std::nullopt; + } + + auto refType = dyn_cast(interfaceSymbolRefOp.getType()); + if (!refType || isa(refType.getElementType())) + { + return std::nullopt; + } + + TypeHelper th(rewriter); + auto loc = interfaceSymbolRefOp.getLoc(); + auto interfaceVal = rewriter.getRemappedValue(interfaceSymbolRefOp.getInterfaceVal()); + auto vtable = rewriter.create(loc, th.getPtrType(), interfaceVal, + MLIRHelper::getStructIndex(rewriter, DATA_VALUE_INDEX)); + auto thisVal = rewriter.create(loc, th.getPtrType(), interfaceVal, + MLIRHelper::getStructIndex(rewriter, THIS_VALUE_INDEX)); + auto slotPtr = rewriter.create( + loc, th.getPtrType(), vtable, interfaceSymbolRefOp.getIndex() + INTERFACE_VTABLE_HEADER_SLOTS); + + auto sizeBits = compileOptions.sizeBits(); + auto intPtrType = rewriter.getIntegerType(sizeBits); + auto slotValue = rewriter.create(loc, intPtrType, slotPtr); + auto zero = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, 0)); + auto isCell = rewriter.create(loc, LLVM::ICmpPredicate::slt, slotValue, zero); + return InterfaceAccessorCell{isCell, thisVal, slotValue, intPtrType, static_cast(sizeBits)}; + } + + // the getter (index 0) or the setter (1) out of the cell + mlir::Value accessor(mlir::Location loc, ConversionPatternRewriter &rewriter, int index) + { + TypeHelper th(rewriter); + auto tag = rewriter.create( + loc, intPtrType, rewriter.getIntegerAttr(intPtrType, llvm::APInt::getSignMask(sizeBits))); + auto cellInt = rewriter.create(loc, intPtrType, slotValue, tag); + auto cellAddr = rewriter.create(loc, th.getPtrType(), cellInt.getResult()); + auto cellLLVMType = LLVM::LLVMStructType::getLiteral(rewriter.getContext(), {th.getPtrType(), th.getPtrType()}, false); + auto accessorPtr = rewriter.create(loc, th.getPtrType(), cellLLVMType, cellAddr, + ArrayRef{0, index}); + return rewriter.create(loc, th.getPtrType(), accessorPtr); + } +}; + // The slot-addressed forms. Same erasure rule, and erasing one of these takes the whole // access with it - there is no load to leave behind in a collected build. class RetainSlotOpLowering : public TsLlvmPattern @@ -583,7 +643,22 @@ class RetainSlotOpLowering : public TsLlvmPattern if (tsLlvmContext->compileOptions.isRefCounted() || (tsLlvmContext->compileOptions.tracksOwnership() && counted)) { OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - orl.emitRetainSlot(cast(op.getSlot().getType()).getElementType(), transformed.getSlot()); + auto retainSlot = [&]() { + orl.emitRetainSlot(cast(op.getSlot().getType()).getElementType(), transformed.getSlot()); + }; + + // an interface's field filled by a getter has no slot: the getter gives its result owned + if (auto accessorCell = InterfaceAccessorCell::of(op.getSlot(), rewriter, tsLlvmContext->compileOptions)) + { + CodeLogicHelper clh(op, rewriter); + clh.conditionalBlocksLowering( + accessorCell->isCell, [&](OpBuilder &, Location) -> ValueRange { return ValueRange{}; }, + [&](OpBuilder &, Location) -> ValueRange { retainSlot(); return ValueRange{}; }); + } + else + { + retainSlot(); + } } rewriter.eraseOp(op); @@ -602,7 +677,22 @@ class ReleaseSlotOpLowering : public TsLlvmPattern if (tsLlvmContext->compileOptions.tracksOwnership()) { OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - orl.emitReleaseSlot(cast(op.getSlot().getType()).getElementType(), transformed.getSlot()); + auto releaseSlot = [&]() { + orl.emitReleaseSlot(cast(op.getSlot().getType()).getElementType(), transformed.getSlot()); + }; + + // an interface's field filled by a setter has no slot: the setter lets go of what it held + if (auto accessorCell = InterfaceAccessorCell::of(op.getSlot(), rewriter, tsLlvmContext->compileOptions)) + { + CodeLogicHelper clh(op, rewriter); + clh.conditionalBlocksLowering( + accessorCell->isCell, [&](OpBuilder &, Location) -> ValueRange { return ValueRange{}; }, + [&](OpBuilder &, Location) -> ValueRange { releaseSlot(); return ValueRange{}; }); + } + else + { + releaseSlot(); + } } rewriter.eraseOp(op); @@ -5018,6 +5108,20 @@ struct LoadOpLowering : public TsLlvmPattern loadedValue = clh.conditionalExpressionLowering(loc, resultTypeLlvm, ptrCmpResult1, createOptionalFunc, undefOptionalFunc); } + else if (auto accessorCell = elementType ? InterfaceAccessorCell::of(loadOp.getReference(), rewriter, tsLlvmContext->compileOptions) + : std::nullopt) + { + // the field filled by a getter: call it + auto getterCallFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + TypeHelper th(rewriter); + auto getter = accessorCell->accessor(loc, rewriter, 0); + auto getterType = LLVM::LLVMFunctionType::get(elementTypeConverted, {th.getPtrType()}); + return rewriter.create(loc, getterType, ValueRange{getter, accessorCell->thisVal}).getResult(); + }; + + loadedValue = clh.conditionalExpressionLowering(loc, elementTypeConverted, accessorCell->isCell, getterCallFunc, + loadedValueFunc); + } else { loadedValue = loadedValueFunc(rewriter, loc); @@ -5133,6 +5237,48 @@ struct StoreOpLowering : public TsLlvmPattern } const bool isInvariantGroup = false; // TODO: Determine if this should be set based on some condition + + // the field filled by a setter: call it; a class with a getter only has nothing to write + if (auto accessorCell = InterfaceAccessorCell::of(storeOp.getReference(), rewriter, tsLlvmContext->compileOptions)) + { + TypeHelper th(rewriter); + CodeLogicHelper clh(storeOp, rewriter); + auto loc = storeOp.getLoc(); + clh.conditionalBlocksLowering( + accessorCell->isCell, + [&](OpBuilder &builder, Location location) -> ValueRange { + auto setter = accessorCell->accessor(loc, rewriter, 1); + auto hasSetter = rewriter.create(loc, LLVM::ICmpPredicate::ne, setter, + rewriter.create(loc, th.getPtrType())); + clh.conditionalBlocksLowering( + hasSetter, + [&](OpBuilder &builder, Location location) -> ValueRange { + auto setterType = LLVM::LLVMFunctionType::get( + th.getVoidType(), {th.getPtrType(), transformed.getValue().getType()}); + rewriter.create(loc, setterType, + ValueRange{setter, accessorCell->thisVal, transformed.getValue()}); + return ValueRange{}; + }, + [&](OpBuilder &builder, Location location) -> ValueRange { return ValueRange{}; }); + + // the value stored is owned by the slot it would have gone to; the setter keeps its own + if (tsLlvmContext->compileOptions.tracksOwnership()) + { + OwnershipRoutineLogic orl(storeOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); + orl.emitReleaseValue(storeOp.getValue().getType(), transformed.getValue()); + } + + return ValueRange{}; + }, + [&](OpBuilder &builder, Location location) -> ValueRange { + rewriter.create(loc, transformed.getValue(), transformed.getReference(), alignment, + isVolatile, isNonTemporal, isInvariantGroup, ordering, syncscope); + return ValueRange{}; + }); + rewriter.eraseOp(storeOp); + return success(); + } + rewriter.replaceOpWithNewOp(storeOp, transformed.getValue(), transformed.getReference(), alignment, isVolatile, isNonTemporal, isInvariantGroup, ordering, syncscope); #ifdef DBG_INFO_ADD_VALUE_OP if (tsLlvmContext->compileOptions.generateDebugInfo) diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 7eb343580..c8a0e249b 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -1513,7 +1513,144 @@ namespace mlirgen } return mlir::success(); - } + } + + // Accessors made for a field's name (mlirGenClassAbstractFieldAccessors, mlirGenClassFieldAccessorsForBase): + // abstract ones with no body, or ones reading and writing the class's field of that name - a member + // access finds a field before an accessor, so `this.prop` in their bodies is the field. A class is + // processed more than once; they are added once. + void MLIRGenImpl::mlirGenClassSynthAccessors(ClassInfo::TypePtr newClassPtr, PropertyDeclaration propertyDeclaration, + mlir::Type type, bool getter, bool setter, bool isAbstract) + { + auto name = propertyDeclaration->name; + auto nameStr = MLIRHelper::getName(name); + if (llvm::any_of(newClassPtr->extraMembersPost, [&](ClassElement member) { + return (member == SyntaxKind::GetAccessor || member == SyntaxKind::SetAccessor) + && MLIRHelper::getName(member.as()->name) == nameStr; + })) + { + return; + } + + NodeFactory nf(NodeFactoryFlags::None); + + auto modifiers = [&]() { + NodeArray modifiers; + for (auto modifier : propertyDeclaration->modifiers) + { + if (modifier == SyntaxKind::PublicKeyword || modifier == SyntaxKind::ProtectedKeyword + || modifier == SyntaxKind::PrivateKeyword) + { + modifiers.push_back(modifier); + } + } + + if (isAbstract) + { + modifiers.push_back(nf.createToken(SyntaxKind::AbstractKeyword)); + } + + return modifiers; + }; + + // the type is known as an mlir type only: named by an alias, as mlirGenClassDataFieldAccessor does + auto typeAlias = StringRef("." + newClassPtr->fullName.str() + "." + nameStr + ".accessor").copy(stringAllocator); + getTypeAliasMap()[typeAlias] = { type, undefined }; + auto typeNode = [&]() { return nf.createTypeReferenceNode(nf.createIdentifier(stows(typeAlias.str())), undefined); }; + + auto thisField = [&]() { return nf.createPropertyAccessExpression(nf.createToken(SyntaxKind::ThisKeyword), name); }; + + if (getter) + { + ts::Block body; + if (!isAbstract) + { + NodeArray statements; + statements.push_back(nf.createReturnStatement(thisField())); + body = nf.createBlock(statements, /*multiLine*/ false); + } + + newClassPtr->extraMembersPost.push_back(nf.createGetAccessorDeclaration(modifiers(), name, {}, typeNode(), body)); + } + + if (setter) + { + ts::Block body; + if (!isAbstract) + { + NodeArray statements; + statements.push_back(nf.createExpressionStatement(nf.createBinaryExpression( + thisField(), nf.createToken(SyntaxKind::EqualsToken), nf.createIdentifier(S("value"))))); + body = nf.createBlock(statements, /*multiLine*/ false); + } + + newClassPtr->extraMembersPost.push_back(nf.createSetAccessorDeclaration( + modifiers(), name, + {nf.createParameterDeclaration(undefined, undefined, nf.createIdentifier(S("value")), undefined, typeNode())}, + body)); + } + } + + // `abstract prop: T` has no storage: it is `abstract get prop(): T` and `abstract set prop(value: T)` (no + // setter when readonly), virtual as accessors are, so a read or write through the base reaches whatever a + // subclass implements it with - accessors of its own, or a field (mlirGenClassFieldAccessorsForBase). + mlir::LogicalResult MLIRGenImpl::mlirGenClassAbstractFieldAccessors(mlir::Location location, ClassInfo::TypePtr newClassPtr, + PropertyDeclaration propertyDeclaration, + const GenContext &genContext) + { + auto [type, init] = mlirGenClassDataFieldType(location, propertyDeclaration, genContext); + if (mth.isNoneType(type)) + { + if (genContext.allowPartialResolve) + { + return mlir::success(); + } + + emitError(location) << "type for abstract field '" << MLIRHelper::getName(propertyDeclaration->name) + << "' must be provided"; + return mlir::failure(); + } + + mlirGenClassSynthAccessors(newClassPtr, propertyDeclaration, type, true, + !hasModifier(propertyDeclaration, SyntaxKind::ReadonlyKeyword), true); + return mlir::success(); + } + + // A field implementing an abstract accessor of a base class (`abstract prop: T` there, or + // `abstract get prop(): T`): the class gets accessors over the field, which fill the base's slots, so a + // read through the base reads the field. Through the class itself the field is read, found first. + void MLIRGenImpl::mlirGenClassFieldAccessorsForBase(ClassInfo::TypePtr newClassPtr, PropertyDeclaration propertyDeclaration) + { + auto nameStr = MLIRHelper::getName(propertyDeclaration->name); + + // the nearest base class's accessor of the name: one implemented on the way is not abstract + std::function nearestAccessor = [&](ClassInfo::TypePtr classInfo) -> AccessorInfo * { + for (auto &base : classInfo->baseClasses) + { + auto index = base->getAccessorIndex(nameStr); + if (index >= 0) + { + return &base->accessors[index]; + } + + if (auto *found = nearestAccessor(base)) + { + return found; + } + } + + return nullptr; + }; + + auto *accessor = nearestAccessor(newClassPtr); + if (!accessor || accessor->isStatic || !accessor->isAbstract) + { + return; + } + + auto type = accessor->get ? accessor->get.funcType.getResult(0) : accessor->set.funcType.getInput(1); + mlirGenClassSynthAccessors(newClassPtr, propertyDeclaration, type, !!accessor->get, !!accessor->set, false); + } std::pair MLIRGenImpl::mlirGenClassDataFieldType(mlir::Location location, PropertyDeclaration propertyDeclaration, const GenContext &genContext) @@ -1583,13 +1720,18 @@ namespace mlirgen { auto accessLevel = getAccessLevel(propertyDeclaration); + if (hasModifier(propertyDeclaration, SyntaxKind::AbstractKeyword)) + { + return mlirGenClassAbstractFieldAccessors(location, newClassPtr, propertyDeclaration, genContext); + } + auto name = propertyDeclaration->name; auto isAccessor = hasModifier(propertyDeclaration, SyntaxKind::AccessorKeyword); if (isAccessor) { name = getFieldNameForAccessor(name); } - + auto fieldId = TupleFieldName(name, genContext); if (!fieldId) { @@ -1654,7 +1796,11 @@ namespace mlirgen { auto res = mlirGenClassDataFieldAccessor(location, newClassPtr, propertyDeclaration, name, type, genContext); EXIT_IF_FAILED(res) - } + } + else + { + mlirGenClassFieldAccessorsForBase(newClassPtr, propertyDeclaration); + } return mlir::success(); } @@ -2581,11 +2727,78 @@ genContext); // and the ones filled by a class field stored as the interface field's function made a hybrid: their // cells carry the field's offset, so a write through the interface (`h.f = g`) stores to the instance llvm::SmallVector writableFieldsForCells; + // the interface's fields (not function-typed) the class implements with accessors (`get prop()`): their + // slots name a cell holding the getter and the setter (see INTERFACE_ACCESSOR_CELL_TAG) + struct AccessorsForField + { + mlir::Attribute id; + MethodInfo get; + MethodInfo set; + }; + llvm::SmallVector accessorsForFields; + // the class's accessor of a name, or its nearest base class's + std::function findAccessor = + [&](ClassInfo::TypePtr classInfo, mlir::StringRef name) -> AccessorInfo * { + auto index = classInfo->getAccessorIndex(name); + if (index >= 0) + { + return classInfo->accessors[index].isStatic ? nullptr : &classInfo->accessors[index]; + } + + for (auto &base : classInfo->baseClasses) + { + if (auto *found = findAccessor(base, name)) + { + return found; + } + } + + return nullptr; + }; auto result = newInterfacePtr->getVirtualTable( virtualTable, [&](mlir::Attribute id, mlir::Type fieldType, bool isConditional) -> std::pair { auto found = false; auto foundField = newClassPtr->findField(id, found); + auto accessorName = dyn_cast(id); + auto *foundAccessor = !found && accessorName && !isa(fieldType) + ? findAccessor(newClassPtr, accessorName.getValue()) : nullptr; + if (foundAccessor) + { + auto getType = foundAccessor->get ? foundAccessor->get.funcType.getResult(0) : mlir::Type(); + auto setType = foundAccessor->set ? foundAccessor->set.funcType.getInput(1) : mlir::Type(); + if (isConditional || (getType && getType != fieldType) || (setType && setType != fieldType)) + { + emitError(location) << "accessor type not matching for " << id << ":" << to_print(fieldType) + << " for interface '" << newInterfacePtr->fullName << "' in class '" + << newClassPtr->fullName << "'" + << (isConditional ? " (an optional field can't be filled by an accessor)" : ""); + return {emptyFieldInfo, mlir::failure()}; + } + + if (llvm::none_of(accessorsForFields, [&](auto &accessorsForField) { return accessorsForField.id == id; })) + { + auto methodInfoOf = [&](FunctionEntry funcEntry) { + MethodInfo methodInfo{}; + if (funcEntry) + { + methodInfo.name = funcEntry.name; + methodInfo.funcName = funcEntry.name; + methodInfo.dllName = funcEntry.dllName; + methodInfo.funcType = funcEntry.funcType; + methodInfo.virtualIndex = -1; + methodInfo.accessLevel = mlir_ts::AccessLevel::Public; + } + + return methodInfo; + }; + + accessorsForFields.push_back({id, methodInfoOf(foundAccessor->get), methodInfoOf(foundAccessor->set)}); + } + + return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; + } + if (!found && isa(fieldType)) { auto fieldName = dyn_cast(id); @@ -2944,6 +3157,63 @@ genContext); } } + // one cell per field filled by accessors: {getter, setter}, either null when the class has none + auto accessorCellName = [&](mlir::Attribute id) { + return concat(fullClassInterfaceVTableFieldName, mlir::cast(id).getValue(), "accessor"); + }; + + for (auto &accessorsForField : accessorsForFields) + { + auto cellType = registerVariable( + location, accessorCellName(accessorsForField.id), true, VariableType::Var, + [&](mlir::Location location, const GenContext &genContext) { + auto isOwnedByDynamicImport = [&](MethodInfo &methodInfo) { + auto owner = methodInfo.funcType ? findMethodOwner(newClassPtr, methodInfo.funcName) : ClassInfo::TypePtr(); + return owner && owner->isDynamicImport; + }; + if (isOwnedByDynamicImport(accessorsForField.get) || isOwnedByDynamicImport(accessorsForField.set)) + { + location = mlir::UnknownLoc::get(builder.getContext()); + } + + auto accessorValue = [&](MethodInfo &methodInfo) -> mlir::Value { + if (!methodInfo.funcType) + { + auto nullValue = builder.create(location, getNullType()); + return cast(location, getOpaqueType(), nullValue, genContext, true); + } + + auto methodValue = methodSymbolValue(location, methodInfo, genContext); + return methodValue ? builder.create(location, getOpaqueType(), methodValue) : mlir::Value(); + }; + + auto getterValue = accessorValue(accessorsForField.get); + auto setterValue = accessorValue(accessorsForField.set); + if (!getterValue || !setterValue) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + mlir::SmallVector cellFields{ + {MLIRHelper::TupleFieldName("get", builder.getContext()), getOpaqueType(), false, + mlir_ts::AccessLevel::Public}, + {MLIRHelper::TupleFieldName("set", builder.getContext()), getOpaqueType(), false, + mlir_ts::AccessLevel::Public}}; + auto cellTupleType = getTupleType(cellFields); + mlir::Value cellValue = builder.create(location, cellTupleType); + cellValue = builder.create(location, cellTupleType, getterValue, cellValue, + MLIRHelper::getStructIndex(builder, 0)); + cellValue = builder.create(location, cellTupleType, setterValue, cellValue, + MLIRHelper::getStructIndex(builder, 1)); + return TypeValueInitType{cellTupleType, cellValue, TypeProvided::Yes}; + }, + genContext); + if (!cellType) + { + return mlir::failure(); + } + } + auto registeredType = registerVariable( location, fullClassInterfaceVTableFieldName, true, VariableType::Var, [&](mlir::Location location, const GenContext &genContext) { @@ -3029,6 +3299,32 @@ genContext); location, virtTuple, slotValue, vtableValue, MLIRHelper::getStructIndex(builder, fieldIndex)); } + else if (methodOrField.isField && llvm::any_of(accessorsForFields, [&](auto &accessorsForField) { + return accessorsForField.id == methodOrField.fieldInfo.id; + })) + { + // a field filled by accessors: the slot is the address of their cell plus + // INTERFACE_ACCESSOR_CELL_TAG, so a load or a store through it calls them + auto cellRef = resolveFullNameIdentifier(location, accessorCellName(methodOrField.fieldInfo.id), true, genContext); + if (!cellRef) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto cellAddress = builder.create(location, ptrIntType, cellRef); + auto tagValue = builder.create( + location, ptrIntType, + builder.getIntegerAttr(ptrIntType, llvm::APInt::getSignMask(compileOptions.sizeBits()))); + 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 (methodOrField.isField) { auto nullObj = builder.create(location, getNullType()); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 9bc10e244..fbd10ad2d 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -12837,6 +12837,14 @@ class MLIRGenImpl mlir::LogicalResult mlirGenClassDataFieldAccessor(mlir::Location location, ClassInfo::TypePtr newClassPtr, PropertyDeclaration propertyDeclaration, MemberName name, mlir::Type typeIfNotProvided, const GenContext &genContext); + void mlirGenClassSynthAccessors(ClassInfo::TypePtr newClassPtr, PropertyDeclaration propertyDeclaration, + mlir::Type type, bool getter, bool setter, bool isAbstract); + + mlir::LogicalResult mlirGenClassAbstractFieldAccessors(mlir::Location location, ClassInfo::TypePtr newClassPtr, + PropertyDeclaration propertyDeclaration, const GenContext &genContext); + + void mlirGenClassFieldAccessorsForBase(ClassInfo::TypePtr newClassPtr, PropertyDeclaration propertyDeclaration); + mlir_ts::AccessLevel getAccessLevel(Node node) { return hasModifier(node, SyntaxKind::PrivateKeyword) ? mlir_ts::AccessLevel::Private diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9a776d402..24769f69b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -607,6 +607,7 @@ tslang_add_test(NAME test-compile-00-class-accessor COMMAND test-runner "${PROJE tslang_add_test(NAME test-compile-00-class-accessor-2 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor2.ts") tslang_add_test(NAME test-compile-00-class-accessor-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") tslang_add_test(NAME test-compile-00-class-accessor-virtual COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts") +tslang_add_test(NAME test-compile-00-class-accessor-implements-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_implements_field.ts") tslang_add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") tslang_add_test(NAME test-compile-00-class-implicit-constructor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_implicit_constructor.ts") tslang_add_test(NAME test-compile-00-class-super-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") @@ -1220,6 +1221,7 @@ tslang_add_test(NAME test-jit-00-class-accessor COMMAND test-runner -jit "${PROJ tslang_add_test(NAME test-jit-00-class-accessor-2 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor2.ts") tslang_add_test(NAME test-jit-00-class-accessor-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") tslang_add_test(NAME test-jit-00-class-accessor-virtual COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts") +tslang_add_test(NAME test-jit-00-class-accessor-implements-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_implements_field.ts") tslang_add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") tslang_add_test(NAME test-jit-00-class-implicit-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_implicit_constructor.ts") tslang_add_test(NAME test-jit-00-class-super-static COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") @@ -2172,6 +2174,7 @@ set(TSLANG_CORPUS 00class_access_control.ts 00class_accessor_super.ts 00class_accessor_virtual.ts + 00class_accessor_implements_field.ts 00class_accessor.ts 00class_accessor2.ts 00class_constr_fields.ts diff --git a/tslang/test/tester/tests/00class_accessor_implements_field.ts b/tslang/test/tester/tests/00class_accessor_implements_field.ts new file mode 100644 index 000000000..2c8055562 --- /dev/null +++ b/tslang/test/tester/tests/00class_accessor_implements_field.ts @@ -0,0 +1,112 @@ +// A getter/setter filling a field of a base class or of an interface (docs/not_impl/not_impl15.ts). +// `abstract prop: T` is an abstract get/set pair, so a read or write through the base reaches what a +// subclass fills it with: accessors of its own, or a field (given accessors over it). Through an interface, +// a field filled by accessors has a slot naming a cell of them (INTERFACE_ACCESSOR_CELL_TAG, Defines.h). +// A read through the base used to read a slot of the base's own that nothing wrote, and print null. + +interface A { + prop: string; + raw: string; + m(): string; +} + +abstract class B implements A { + abstract prop: string; + abstract raw: string; + abstract readonly ro: string; + abstract get readonlyProp(): string; + abstract set readonlyProp(val: string); + abstract m(): string; + + describe() { + return this.prop + "/" + this.raw + "/" + this.ro + "/" + this.readonlyProp; + } +} + +class C extends B { + written = ""; + get prop() { return "foo"; } + set prop(v: string) { this.written = v; } + raw = "edge"; + readonly ro = "readonly please"; + readonlyProp: string = "rp"; + m() { return "m"; } +} + +interface Named { + name: string; + count: number; +} + +class Accessors implements Named { + _name = "n"; + sets = 0; + get name() { return this._name + "!"; } + set name(v: string) { this._name = v; this.sets++; } + count: number = 3; +} + +class Fields implements Named { + name = "plain"; + count: number = 4; +} + +class GetterOnly implements Named { + get name() { return "fixed"; } + get count(): number { return 5; } +} + +function readName(n: Named) { + return n.name; +} + +function main() { + const c = new C(); + + // through the abstract base + const b: B = c; + assert(b.prop == "foo"); + b.prop = "x"; + assert(c.written == "x"); + assert(b.raw == "edge"); + b.raw = "y"; + assert(c.raw == "y"); + assert(b.ro == "readonly please"); + assert(b.readonlyProp == "rp"); + b.readonlyProp = "rp2"; + assert(c.readonlyProp == "rp2"); + assert(b.describe() == "foo/y/readonly please/rp2"); + + // through an interface the base implements + const a: A = c; + assert(a.prop == "foo"); + a.prop = "z"; + assert(c.written == "z"); + assert(a.raw == "y"); + a.raw = "w"; + assert(c.raw == "w"); + assert(a.m() == "m"); + + // through an interface the class implements, with accessors or with fields + const acc = new Accessors(); + const n: Named = acc; + assert(n.name == "n!"); + n.name = "v"; + assert(acc.sets == 1); + assert(n.name == "v!"); + assert(n.count == 3); + n.count = 6; + assert(acc.count == 6); + + assert(readName(acc) == "v!"); + assert(readName(new Fields()) == "plain"); + + // a getter only: a write through the interface has nothing to call + const g: Named = new GetterOnly(); + assert(g.name == "fixed"); + assert(g.count == 5); + g.name = "ignored"; + assert(g.name == "fixed"); + + print("done."); +} From e475e6c0e22e72a226d9535c14b8cf424b7c6644 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 18:20:08 +0000 Subject: [PATCH 2/3] Remove docs/not_impl not_impl15 (accessors filling abstract and interface fields) and not_impl28 (contextually typed lambdas in object literals) are implemented on this branch and covered by tests; the rest are dropped by request. Co-Authored-By: Claude Opus 5.5 --- docs/not_impl/not_impl12.ts | 13 ---- docs/not_impl/not_impl13.ts | 8 --- docs/not_impl/not_impl15.ts | 22 ------- docs/not_impl/not_impl27.ts | 125 ------------------------------------ docs/not_impl/not_impl28.ts | 43 ------------- docs/not_impl/not_impl30.ts | 69 -------------------- docs/not_impl/not_impl5.ts | 13 ---- docs/not_impl/not_impl7.ts | 26 -------- docs/not_impl/not_impl9.ts | 8 --- 9 files changed, 327 deletions(-) delete mode 100644 docs/not_impl/not_impl12.ts delete mode 100644 docs/not_impl/not_impl13.ts delete mode 100644 docs/not_impl/not_impl15.ts delete mode 100644 docs/not_impl/not_impl27.ts delete mode 100644 docs/not_impl/not_impl28.ts delete mode 100644 docs/not_impl/not_impl30.ts delete mode 100644 docs/not_impl/not_impl5.ts delete mode 100644 docs/not_impl/not_impl7.ts delete mode 100644 docs/not_impl/not_impl9.ts diff --git a/docs/not_impl/not_impl12.ts b/docs/not_impl/not_impl12.ts deleted file mode 100644 index a78017a1a..000000000 --- a/docs/not_impl/not_impl12.ts +++ /dev/null @@ -1,13 +0,0 @@ -function testLoopScope() { - for (let i = 0; i < 3; ++i) { - let val: number; - // TODO: - assert(val === undefined, "loopscope"); - val = i; - } -} - -function main() { - testLoopScope(); - print("done."); -} \ No newline at end of file diff --git a/docs/not_impl/not_impl13.ts b/docs/not_impl/not_impl13.ts deleted file mode 100644 index 93f9a9786..000000000 --- a/docs/not_impl/not_impl13.ts +++ /dev/null @@ -1,8 +0,0 @@ -class ConcreteA {} -class ConcreteB {} - -function main() -{ - [ConcreteA, ConcreteB].map(cls => new cls()); // should work - print("done."); -} \ No newline at end of file diff --git a/docs/not_impl/not_impl15.ts b/docs/not_impl/not_impl15.ts deleted file mode 100644 index c451aab15..000000000 --- a/docs/not_impl/not_impl15.ts +++ /dev/null @@ -1,22 +0,0 @@ -//@target: ES5 -interface A { - prop: string; - raw: string; - m(): void; -} -abstract class B implements A { - abstract prop: string; - abstract raw: string; - abstract readonly ro: string; - abstract get readonlyProp(): string; - abstract set readonlyProp(val: string); - abstract m(): void; -} -class C extends B { - get prop() { return "foo"; } - set prop(v) { } - raw = "edge"; - readonly ro = "readonly please"; - readonlyProp: string; // don't have to give a value, in fact - m() { } -} \ No newline at end of file diff --git a/docs/not_impl/not_impl27.ts b/docs/not_impl/not_impl27.ts deleted file mode 100644 index 4e9586465..000000000 --- a/docs/not_impl/not_impl27.ts +++ /dev/null @@ -1,125 +0,0 @@ -type Window = any; -type window = any; - - -// Generic call with no parameters -function noParams() { } - -// Generic call with parameters but none use type parameter type -function noGenericParams(n: string) { } - -// Generic call with multiple type parameters and only one used in parameter type annotation -function someGenerics1(n: T, m: number) { } - -// Generic call with argument of function type whose parameter is of type parameter type -function someGenerics2a(n: (x: T) => void) { } - -function someGenerics2b(n: (x: T, y: U) => void) { } - -// Generic call with argument of function type whose parameter is not of type parameter type but body/return type uses type parameter -function someGenerics3(producer: () => T) { } - -// 2 parameter generic call with argument 1 of type parameter type and argument 2 of function type whose parameter is of type parameter type -function someGenerics4(n: T, f: (x: U) => void) { } - -// 2 parameter generic call with argument 2 of type parameter type and argument 1 of function type whose parameter is of type parameter type -function someGenerics5(n: T, f: (x: U) => void) { } - -// Generic call with multiple arguments of function types that each have parameters of the same generic type -function someGenerics6(a: (a: A) => A, b: (b: A) => A, c: (c: A) => A) { } - -// Generic call with multiple arguments of function types that each have parameters of different generic type -function someGenerics7(a: (a: A) => A, b: (b: B) => B, c: (c: C) => C) { } - -// Generic call with argument of generic function type -function someGenerics8(n: T): T { return n; } - -// Generic call with multiple parameters of generic type passed arguments with no best common type -function someGenerics9(a: T, b: T, c: T): T { - return null; -} - -// Generic call with multiple parameters of generic type passed arguments with multiple best common types -interface A91 { - x: number; - y?: string; -} -interface A92 { - x: number; - z?: Window; -} - -function main() { - - noParams(); - noParams(); - noParams<{}>(); - - noGenericParams(''); // Valid - noGenericParams(''); - //noGenericParams<{}>(''); // Error - - someGenerics1(3, 4); // Valid - //someGenerics1(3, 4); // Error - //someGenerics1(3, 4); // Error - someGenerics1(3, 4); - - someGenerics2a((n: string) => n); - someGenerics2a((n: string) => n); - someGenerics2a((n) => n.substr(0)); - - someGenerics2b((n: string, x: number) => n); - someGenerics2b((n: string, t: number) => n); - someGenerics2b((n, t) => n.substr(t * t)); - - //someGenerics3(() => ''); // Error - someGenerics3(() => undefined); - //someGenerics3(() => 3); // Error - - someGenerics4(4, () => null); // Valid - someGenerics4('', () => 3); - //someGenerics4('', (x: string) => ''); // Error - someGenerics4(null, null); - - someGenerics5(4, () => null); // Valid - someGenerics5('', () => 3); - //someGenerics5('', (x: string) => ''); // Error - //someGenerics5(null, null); // Error - - someGenerics6(n => n, n => n, n => n); // Valid - someGenerics6(n => n, n => n, n => n); - //someGenerics6((n: number) => n, (n: string) => n, (n: number) => n); // Error - someGenerics6((n: number) => n, (n: number) => n, (n: number) => n); - - someGenerics7(n => n, n => n, n => n); // Valid, types of n are respectively - someGenerics7(n => n, n => n, n => n); - someGenerics7((n: number) => n, (n: string) => n, (n: number) => n); - - //let x = someGenerics8(someGenerics7); // Error - //x(null, null, null); // Error - - let a9a = someGenerics9('', 0, []); - //let a9a: {}; - let a9b = someGenerics9<{ a?: number; b?: string; }>({ a: 0 }, { b: '' }, null); - //let a9b: { a?: number; b?: string; }; - - let a9e = someGenerics9(undefined, { x: 6, z: window }, { x: 6, y: '' }); - //let a9e: {}; - let a9f = someGenerics9(undefined, { x: 6, z: window }, { x: 6, y: '' }); - //let a9f: A92; - - // Generic call with multiple parameters of generic type passed arguments with a single best common type - let a9d = someGenerics9({ x: 3 }, { x: 6 }, { x: 6 }); - //let a9d: { x: number; }; - - // Generic call with multiple parameters of generic type where one argument is of type 'any' - let anyVar: any; - let a = someGenerics9(7, anyVar, 4); - //let a: any; - - // Generic call with multiple parameters of generic type where one argument is [] and the other is not 'any' - let arr = someGenerics9([], null, undefined); - //let arr: any[]; - - print("done."); -} \ No newline at end of file diff --git a/docs/not_impl/not_impl28.ts b/docs/not_impl/not_impl28.ts deleted file mode 100644 index 90002fabe..000000000 --- a/docs/not_impl/not_impl28.ts +++ /dev/null @@ -1,43 +0,0 @@ -interface Computed { - read(): T; - write(value: T); -} - -function foo(x: Computed) { } - -enum E1 { X } -enum E2 { X } - -// Check that we infer from both a.r and b before fixing T in a.w - -function f1(a: { w: (x: T) => U; r: () => T; }, b: T): U -{ - return w(b); -} - -function main() { - - let s: string; - - // Calls below should infer string for T and then assign that type to the value parameter - foo({ - read: () => s, - write: value => s = value - }); - foo({ - write: value => s = value, - read: () => s - }); - - - let v1: number; - v1 = f1({ w: x => x, r: () => 0 }, 0); - v1 = f1({ w: x => x, r: () => 0 }, E1.X); - v1 = f1({ w: x => x, r: () => E1.X }, 0); - - let v2: E1; - v2 = f1({ w: x => x, r: () => E1.X }, E1.X); - - //let v3 = f1({ w: x => x, r: () => E1.X }, E2.X); // Error - -} \ No newline at end of file diff --git a/docs/not_impl/not_impl30.ts b/docs/not_impl/not_impl30.ts deleted file mode 100644 index db4833e7a..000000000 --- a/docs/not_impl/not_impl30.ts +++ /dev/null @@ -1,69 +0,0 @@ -// A discriminated type union is where you use code flow -// analysis to reduce a set of potential objects down to one -// specific object. -// -// This pattern works really well for sets of similar -// objects with a different string or number constant -// for example: a list of named events, or versioned -// sets of objects. - -type TimingEvent = { name: "start"; userStarted: boolean } | { name: "closed"; duration: number }; - -// When event comes into this function, it could be any -// of the two potential types. - -const handleEvent = (event: TimingEvent) => { - // By using a switch against event.name TypeScript's code - // flow analysis can determine that an object can only - // be represented by one type in the union. - - switch (event.name) { - case "start": - // This means you can safely access userStarted - // because it's the only type inside TimingEvent - // where name is "start" - const initiatedByUser = event.userStarted; - break; - - case "closed": - const timespan = event.duration; - break; - } -}; - -// This pattern is the same with numbers which we can use -// as the discriminator. - -// In this example, we have a discriminate union and an -// additional error state to handle. - -type APIResponses = { version: 0; msg: string } | { version: 1; message: string; status: number } | { error: string }; - -const handleResponse = (response: APIResponses) => { - // Handle the error case, and then return - if ("error" in response) { - print(response.error); - return; - } - - // TypeScript now knows that APIResponse cannot be - // the error type. If it were the error, the function - // would have returned. You can verify this by - // hovering over response below. - - if (response.version === 0) { - print(response.msg); - } else if (response.version === 1) { - print(response.status, response.message); - } -}; - -// You're better off using a switch statement instead of -// if statements because you can make assurances that all -// parts of the union are checked. There is a good pattern -// for this using the never type in the handbook: - -// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions - -function main() { -} \ No newline at end of file diff --git a/docs/not_impl/not_impl5.ts b/docs/not_impl/not_impl5.ts deleted file mode 100644 index 27b42df4c..000000000 --- a/docs/not_impl/not_impl5.ts +++ /dev/null @@ -1,13 +0,0 @@ -function getPointFactory(x: number, y: number) { - class P { - x = x; - y = y; - } - - return P; -} - -function main() { - const PointZero = getPointFactory(0, 0); - const PointOne = getPointFactory(1, 1); -} \ No newline at end of file diff --git a/docs/not_impl/not_impl7.ts b/docs/not_impl/not_impl7.ts deleted file mode 100644 index e2ef9217c..000000000 --- a/docs/not_impl/not_impl7.ts +++ /dev/null @@ -1,26 +0,0 @@ -let p: Promise; - -async function fn(): Promise { - const i = await p; // suspend execution until 'p' is settled. 'i' has type "number" - return 1 + i; -} - -class C { - async m(): Promise { - const i = await p; // suspend execution until 'p' is settled. 'i' has type "number" - return 1 + i; - } - async get p(): Promise { - const i = await p; // suspend execution until 'p' is settled. 'i' has type "number" - return 1 + i; - } -} - -function main() { - const a = async (): Promise => 1 + await p; // suspends execution. - const a2 = async () => 1 + await p; // suspends execution. return type is inferred as "Promise" when compiling with --target ES6 - const fe = async function (): Promise { - const i = await p; // suspend execution until 'p' is settled. 'i' has type "number" - return 1 + i; - } -} \ No newline at end of file diff --git a/docs/not_impl/not_impl9.ts b/docs/not_impl/not_impl9.ts deleted file mode 100644 index 20eedc9c0..000000000 --- a/docs/not_impl/not_impl9.ts +++ /dev/null @@ -1,8 +0,0 @@ -interface Observable { - map(proj: (el: T) => U): Observable; -} - -function main() { - let o: Observable; - o.map(); -} \ No newline at end of file From 75b2bb51df746b1c1ff107be4c947722f40a4939 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 28 Oct 2026 18:22:16 +0000 Subject: [PATCH 3/3] Untyped lambda parameters in object literals take their types from the receiver `const o: { w: (x: number) => number } = { w: x => x + 1 }` failed with "can't find type by name: TGenParam_x": an object literal's function members never saw the receiver, so an untyped parameter had no type (docs/not_impl/not_impl28.ts). - A function member of an object literal takes its parameter types (and an unannotated result type) from the receiver's field of that name, or its interface's method less `this`. - A generic call with such a literal for an argument (`foo({ read: () => s, write: value => s = value })` for `foo(x: Computed)`) infers first, with no code made, from the other arguments and the literal's other members (an expression-bodied lambda's type is its parameters' and its expression's); the parameter's type with those type parameters known is the literal's receiver. One still holding a type parameter (`U` of `w: (x: T) => U`) types the members' parameters only. - Inference matches an interface template against an object (`Computed` from `{ read: () => string }`), and a function with no parameters infers from its result (`() => T` from `() => string`). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 197 +++++++++++++++++- tslang/lib/TypeScript/MLIRGenGenerics.cpp | 70 ++++++- tslang/lib/TypeScript/MLIRGenImpl.h | 72 ++++++- tslang/test/tester/CMakeLists.txt | 3 + .../00object_literal_contextual_lambda.ts | 63 ++++++ 5 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 tslang/test/tester/tests/00object_literal_contextual_lambda.ts diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index bff71b465..13fc862ec 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1253,7 +1253,14 @@ namespace mlirgen } } - if (mlir::failed(mlirGenOperands(callExpression->arguments, operands, operandsFuncType, genContext, offsetArgs, noReceiverTypesForGenericCall))) + SmallVector argumentReceivers; + if (noReceiverTypesForGenericCall) + { + inferObjectLiteralArgumentReceivers(location, funcResult, callExpression->arguments, argumentReceivers, genContext); + } + + if (mlir::failed(mlirGenOperands(callExpression->arguments, operands, operandsFuncType, genContext, offsetArgs, + noReceiverTypesForGenericCall, argumentReceivers))) { return mlir::failure(); } @@ -1266,6 +1273,189 @@ namespace mlirgen return mlirGenCallExpression(location, funcResult, callExpression->typeArguments, operands, genContext); } + // An object literal with a function member whose parameters are not all typed (`{ write: value => s = value }`): + // they take their types from the literal's receiver + static bool isContextSensitiveObjectLiteral(Expression expression) + { + if (expression != SyntaxKind::ObjectLiteralExpression) + { + return false; + } + + return llvm::any_of(expression.as()->properties, [](ObjectLiteralElementLike item) { + Node member = item; + if (item == SyntaxKind::PropertyAssignment) + { + member = item.as()->initializer; + } + + if (member != SyntaxKind::ArrowFunction && member != SyntaxKind::FunctionExpression + && member != SyntaxKind::MethodDeclaration) + { + return false; + } + + return llvm::any_of(member.as()->parameters, + [](ParameterDeclaration parameter) { return !parameter->type && !parameter->initializer; }); + }); + } + + // A generic call with such a literal for an argument: `foo({ read: () => s, write: value => s = value })` for + // `foo(x: Computed)`. The members' parameters are of T, which the rest of the call gives - the other + // arguments, and the literal's other members. Inferred from their types first (no code is made), the + // parameter's type with T known is the literal's receiver, which types the members' parameters + // (getObjectLiteralMemberReceiverFuncType). A receiver that can't be known is left out; one with type parameters + // still unknown types the members' parameters only (mlirGenOperand). + void MLIRGenImpl::inferObjectLiteralArgumentReceivers(mlir::Location location, mlir::Value funcResult, + NodeArray arguments, SmallVector &receivers, + const GenContext &genContext) + { + auto symbolOp = funcResult.getDefiningOp(); + if (!symbolOp || !symbolOp->hasAttrOfType(GENERIC_ATTR_NAME) + || llvm::none_of(arguments, isContextSensitiveObjectLiteral) || !genContext.postponedMessages) + { + return; + } + + auto functionGenericTypeInfo = getGenericFunctionInfoByFullName(symbolOp.getIdentifierAttr().getValue()); + if (!functionGenericTypeInfo || !functionGenericTypeInfo->funcOp || !functionGenericTypeInfo->functionDeclaration) + { + return; + } + + auto params = functionGenericTypeInfo->funcOp->getParams(); + auto declarationParams = functionGenericTypeInfo->functionDeclaration->parameters; + if (params.size() < declarationParams.size()) + { + return; + } + + // `this` ahead of the declared parameters + auto offset = params.size() - declarationParams.size(); + + // what is tried here and fails is the call's to report + auto savedErrorMessagesCount = genContext.postponedMessages->size(); + + StringMap inferredTypes; + for (auto [index, argument] : enumerate(arguments)) + { + if (index >= declarationParams.size() || declarationParams[index]->dotDotDotToken) + { + break; + } + + auto paramType = params[index + offset]->getType(); + if (argument == SyntaxKind::ArrowFunction || argument == SyntaxKind::FunctionExpression) + { + // a lambda's own types are inferred with the call + continue; + } + + if (!isContextSensitiveObjectLiteral(argument)) + { + // widened, as an inference from a literal is (`5` gives s32, not the literal type 5) + if (auto type = evaluate(argument, genContext)) + { + inferType(location, paramType, mth.wideStorageType(type), inferredTypes, genContext); + } + continue; + } + + // the literal's members whose types don't wait for T + SmallVector fields; + for (auto &item : argument.as()->properties) + { + if (item != SyntaxKind::PropertyAssignment || isContextSensitiveObjectLiteral(item.as()->initializer)) + { + continue; + } + + auto propertyAssignment = item.as(); + auto initializer = propertyAssignment->initializer; + mlir::Type type; + if (initializer == SyntaxKind::ArrowFunction || initializer == SyntaxKind::FunctionExpression) + { + // a function made now would be made again with the literal: its type is its parameters' and, + // for `() => s`, its expression's + auto funcLikeDeclaration = initializer.as(); + if (!funcLikeDeclaration->body || funcLikeDeclaration->body == SyntaxKind::Block + || llvm::any_of(funcLikeDeclaration->parameters, [](ParameterDeclaration parameter) { return !parameter->type; })) + { + continue; + } + + SmallVector inputs; + for (auto parameter : funcLikeDeclaration->parameters) + { + inputs.push_back(getType(parameter->type, genContext)); + } + + auto resultType = evaluate(funcLikeDeclaration->body.as(), genContext); + if (!resultType || mth.isNoneType(resultType) || llvm::any_of(inputs, [](mlir::Type input) { return !input; })) + { + continue; + } + + type = getFunctionType(inputs, {mth.wideStorageType(resultType)}, false); + } + else + { + type = evaluate(initializer, genContext); + type = type ? mth.wideStorageType(type) : type; + } + + auto fieldId = TupleFieldName(propertyAssignment->name, genContext); + if (fieldId && type && !mth.isNoneType(type)) + { + fields.push_back({fieldId, type, false, mlir_ts::AccessLevel::Public}); + } + } + + if (!fields.empty()) + { + inferType(location, paramType, getTupleType(fields), inferredTypes, genContext); + } + } + + GenContext typeGenContext(genContext); + typeGenContext.typeParamsWithArgs = functionGenericTypeInfo->typeParamsWithArgs; + auto anyNamedGenericType = IsGeneric::False; + if (mlir::succeeded(appendInferredTypes(location, functionGenericTypeInfo->typeParams, inferredTypes, + anyNamedGenericType, typeGenContext, false, true))) + { + // a type parameter not known yet (`U` of `w: (x: T) => U`, given by `w` itself) stays one + for (auto &typeParam : functionGenericTypeInfo->typeParams) + { + if (!typeGenContext.typeParamsWithArgs.count(typeParam->getName())) + { + typeGenContext.typeParamsWithArgs[typeParam->getName()] = {typeParam, getNamedGenericType(typeParam->getName())}; + } + } + + MLIRNamespaceGuard ng(currentNamespace); + currentNamespace = functionGenericTypeInfo->elementNamespace; + SourceFileScope sourceFileScope(*this, functionGenericTypeInfo->sourceFile, functionGenericTypeInfo->fileName); + + receivers.resize(arguments.size()); + for (auto [index, argument] : enumerate(arguments)) + { + if (index >= declarationParams.size() || !declarationParams[index]->type + || !isContextSensitiveObjectLiteral(argument)) + { + continue; + } + + auto type = getType(declarationParams[index]->type, typeGenContext); + if (type && !mth.isNoneType(type)) + { + receivers[index] = type; + } + } + } + + rollbackPostponedErrorMessages(genContext.postponedMessages, savedErrorMessagesCount); + } + // `Shared.count(s)` (own spec 23.1): the number of handles under rc and own, -1 under gc and none. ValueOrLogicalResult MLIRGenImpl::mlirGenSharedCount(mlir::Location location, CallExpression callExpression, const GenContext &genContext) @@ -2169,6 +2359,11 @@ namespace mlirgen oli.objectLiteral = objectLiteral; oli.receiverType = genContext.receiverType; + if (!oli.receiverType && genContext.receiverFuncType && !mth.isAnyFunctionType(genContext.receiverFuncType)) + { + oli.memberReceiverType = mth.stripOptionalType(genContext.receiverFuncType); + } + if (oli.receiverType) { oli.receiverType = mth.stripOptionalType(oli.receiverType); diff --git a/tslang/lib/TypeScript/MLIRGenGenerics.cpp b/tslang/lib/TypeScript/MLIRGenGenerics.cpp index bc3c28349..b6d355178 100644 --- a/tslang/lib/TypeScript/MLIRGenGenerics.cpp +++ b/tslang/lib/TypeScript/MLIRGenGenerics.cpp @@ -92,6 +92,11 @@ namespace mlirgen StringMap &results, const GenContext &genContext) { auto tempInterface = dyn_cast(templateType); + if (tempInterface && !isa(concreteType)) + { + return tryInferInterfaceFromObject(location, tempInterface, concreteType, results, genContext); + } + auto typeInterface = dyn_cast(concreteType); if (!tempInterface || !typeInterface) { @@ -134,6 +139,66 @@ namespace mlirgen return false; } + // `Computed` from an object literal (`{ read: () => s, write: (v: string) => ... }`): each member of + // the object infers from the interface's member of that name - a field from its type, a method from + // its function type, the `this` both take aside. + bool MLIRGenImpl::tryInferInterfaceFromObject(mlir::Location location, mlir_ts::InterfaceType tempInterface, + mlir::Type concreteType, StringMap &results, + const GenContext &genContext) + { + if (auto objectType = dyn_cast(concreteType)) + { + concreteType = objectType.getStorageType(); + } + + concreteType = MLIRTypeHelper::stripValueType(concreteType); + if (!isa(concreteType)) + { + return false; + } + + auto tempInterfaceInfo = getInterfaceInfoByFullName(tempInterface.getName().getValue()); + llvm::SmallVector concreteFields; + if (!tempInterfaceInfo || mlir::failed(mth.getFields(concreteType, concreteFields))) + { + return false; + } + + // the function type without its `this` (the first input), so an interface's method and an object's match + auto withoutThis = [&](mlir::Type type) -> mlir::Type { + auto funcType = dyn_cast(type); + if (!funcType || funcType.getNumInputs() == 0) + { + return type; + } + + return getFunctionType(funcType.getInputs().drop_front(), funcType.getResults(), funcType.isVarArg()); + }; + + for (auto &concreteField : concreteFields) + { + int vtableOffset = 0; + if (auto *fieldInfo = tempInterfaceInfo->findField(concreteField.id, vtableOffset)) + { + inferType(location, fieldInfo->type, concreteField.type, results, genContext); + continue; + } + + auto name = dyn_cast(concreteField.id); + auto *methodInfo = name ? tempInterfaceInfo->findMethod(name.getValue(), vtableOffset) : nullptr; + if (methodInfo && methodInfo->funcType) + { + // an object's method takes the object as its `this`, a closure (an arrow function) does not + auto concreteFuncType = dyn_cast(concreteField.type); + auto concreteType = concreteFuncType && concreteFuncType.getNumInputs() == methodInfo->funcType.getNumInputs() + ? withoutThis(concreteField.type) : concreteField.type; + inferType(location, withoutThis(methodInfo->funcType), concreteType, results, genContext); + } + } + + return true; + } + bool MLIRGenImpl::tryInferArray(mlir::Location location, mlir::Type templateType, mlir::Type concreteType, StringMap &results, const GenContext &genContext) { @@ -236,11 +301,12 @@ namespace mlirgen return false; } + // `() => T` from `() => string` too: a function with no parameters infers from its result auto tempfuncType = mth.getParamsFromFuncRef(templateType); - if (tempfuncType.size() > 0) + if (tempfuncType.size() > 0 || mth.getReturnsFromFuncRef(templateType).size() > 0) { auto funcType = mth.getParamsFromFuncRef(concreteType); - if (funcType.size() > 0) + if (funcType.size() > 0 || tempfuncType.size() == 0) { inferTypeFuncType(location, tempfuncType, funcType, results, genContext); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index fbd10ad2d..8c48b7011 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1552,6 +1552,8 @@ class MLIRGenImpl bool tryInferInterface(mlir::Location location, mlir::Type templateType, mlir::Type concreteType, StringMap &results, const GenContext &genContext); + bool tryInferInterfaceFromObject(mlir::Location location, mlir_ts::InterfaceType tempInterface, mlir::Type concreteType, + StringMap &results, const GenContext &genContext); bool tryInferArray(mlir::Location location, mlir::Type templateType, mlir::Type concreteType, StringMap &results, const GenContext &genContext); @@ -9284,6 +9286,8 @@ class MLIRGenImpl ValueOrLogicalResult mlirGenElementAccess(mlir::Location location, mlir::Value expression, mlir::Value argumentExpression, bool isConditionalAccess, const GenContext &genContext); ValueOrLogicalResult mlirGen(CallExpression callExpression, const GenContext &genContext); + void inferObjectLiteralArgumentReceivers(mlir::Location location, mlir::Value funcResult, NodeArray arguments, + SmallVector &receivers, const GenContext &genContext); mlir::LogicalResult mlirGenArrayForEach(mlir::Location location, ArrayRef operands, const GenContext &outerContext) @@ -10100,11 +10104,19 @@ class MLIRGenImpl mlir::LogicalResult processOperandSpreadElement(mlir::Location location, mlir::Value source, OperandsProcessingInfo &operandsProcessingInfo, const GenContext &genContext); - mlir::LogicalResult mlirGenOperand(Expression expression, OperandsProcessingInfo &operandsProcessingInfo, const GenContext &genContext) + mlir::LogicalResult mlirGenOperand(Expression expression, OperandsProcessingInfo &operandsProcessingInfo, const GenContext &genContext, + mlir::Type receiverType = mlir::Type()) { GenContext argGenContext(genContext); argGenContext.clearReceiverTypes(); operandsProcessingInfo.setReceiverTo(argGenContext); + // a receiver inferred ahead of the call (inferObjectLiteralArgumentReceivers); one still of a type parameter + // types an object literal's function members only (ObjectLiteralInfo::memberReceiverType) + if (receiverType) + { + argGenContext.receiverType = mth.isGenericType(receiverType) ? mlir::Type() : receiverType; + argGenContext.receiverFuncType = receiverType; + } auto result = mlirGen(expression, argGenContext); EXIT_IF_FAILED_OR_NO_VALUE(result) @@ -10163,7 +10175,8 @@ class MLIRGenImpl // TODO: rewrite code (do as clean as ArrayLiteral) mlir::LogicalResult mlirGenOperands(NodeArray arguments, SmallVector &operands, - mlir::Type funcType, const GenContext &genContext, int offsetArgs = 0, bool noReceiverTypesForGenericCall = false) + mlir::Type funcType, const GenContext &genContext, int offsetArgs = 0, bool noReceiverTypesForGenericCall = false, + ArrayRef argumentReceivers = {}) { OperandsProcessingInfo operandsProcessingInfo(funcType, operands, offsetArgs, noReceiverTypesForGenericCall, mth, genContext.disableSpreadParams); @@ -10175,7 +10188,9 @@ class MLIRGenImpl return mlirGenOperandVarArgs(loc(arguments), proccessedArgs, arguments, operandsProcessingInfo, genContext); } - if (mlir::failed(mlirGenOperand(*it, operandsProcessingInfo, genContext))) + auto index = static_cast(std::distance(arguments.begin(), it)); + if (mlir::failed(mlirGenOperand(*it, operandsProcessingInfo, genContext, + index < argumentReceivers.size() ? argumentReceivers[index] : mlir::Type()))) { return mlir::failure(); } @@ -11555,6 +11570,9 @@ class MLIRGenImpl ts::ObjectLiteralExpression objectLiteral; mlir_ts::ObjectType objThis; mlir::Type receiverType; + // what the untyped parameters of its function members take their types from when it has no receiver: one + // still of a type parameter (`{ w: (x: T) => U }` of a generic call) + mlir::Type memberReceiverType; SmallVector fieldInfos; SmallVector values; SmallVector methodInfos; @@ -11679,12 +11697,48 @@ class MLIRGenImpl return mlir::success(); } + // The function type an object literal's function member takes the types of its untyped parameters from + // (`{ w: x => x + 1 }`): the receiver's field of that name (`w: (x: number) => number`), or its interface's + // method (`w(x: number): number`, less its `this`). None when the receiver has no such member. + mlir::Type getObjectLiteralMemberReceiverFuncType(ObjectLiteralInfo &oli, mlir::Attribute fieldId) + { + auto receiverType = oli.receiverType ? oli.receiverType : oli.memberReceiverType; + if (!receiverType || !fieldId) + { + return mlir::Type(); + } + + auto fieldType = getTypeByFieldNameFromReceiverType(fieldId, receiverType); + if (fieldType && mth.isAnyFunctionType(mth.stripOptionalType(fieldType))) + { + return mth.stripOptionalType(fieldType); + } + + auto interfaceType = dyn_cast(receiverType); + auto name = dyn_cast(fieldId); + if (!interfaceType || !name) + { + return mlir::Type(); + } + + auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); + auto *methodInfo = interfaceInfo ? interfaceInfo->findMethod(name.getValue()) : nullptr; + if (!methodInfo || !methodInfo->funcType || methodInfo->funcType.getNumInputs() == 0) + { + return mlir::Type(); + } + + auto funcType = methodInfo->funcType; + return getFunctionType(funcType.getInputs().drop_front(), funcType.getResults(), funcType.isVarArg()); + } + mlir::LogicalResult processObjectFunctionLikeProto(ObjectLiteralInfo &oli, mlir::Attribute fieldId, FunctionLikeDeclarationBase funcLikeDecl, const GenContext &genContext) { auto funcGenContext = GenContext(genContext); funcGenContext.clearScopeVars(); funcGenContext.clearReceiverTypes(); + funcGenContext.receiverFuncType = getObjectLiteralMemberReceiverFuncType(oli, fieldId); funcGenContext.thisType = oli.objThis; funcLikeDecl->parent = oli.objectLiteral; @@ -11725,12 +11779,13 @@ class MLIRGenImpl return addObjectFuncFieldInfo(oli, fieldId, funcName, funcType); } - mlir::LogicalResult processObjectFunctionLike(ObjectLiteralInfo &oli, FunctionLikeDeclarationBase funcLikeDecl, - const GenContext &genContext) + mlir::LogicalResult processObjectFunctionLike(ObjectLiteralInfo &oli, mlir::Attribute fieldId, + FunctionLikeDeclarationBase funcLikeDecl, const GenContext &genContext) { auto funcGenContext = GenContext(genContext); funcGenContext.clearScopeVars(); funcGenContext.clearReceiverTypes(); + funcGenContext.receiverFuncType = getObjectLiteralMemberReceiverFuncType(oli, fieldId); funcGenContext.thisType = oli.objThis; LLVM_DEBUG(llvm::dbgs() << "\n!! Object Process function with this type: " << oli.objThis << "\n";); @@ -12149,7 +12204,7 @@ class MLIRGenImpl } auto funcLikeDecl = propertyAssignment->initializer.as(); - if (mlir::failed(processObjectFunctionLike(oli, funcLikeDecl, genContext))) { + if (mlir::failed(processObjectFunctionLike(oli, TupleFieldName(propertyAssignment->name, genContext), funcLikeDecl, genContext))) { return mlir::failure(); } } @@ -12163,14 +12218,15 @@ class MLIRGenImpl } auto funcLikeDecl = shorthandPropertyAssignment->initializer.as(); - if (mlir::failed(processObjectFunctionLike(oli, funcLikeDecl, genContext))) { + if (mlir::failed(processObjectFunctionLike(oli, TupleFieldName(shorthandPropertyAssignment->name, genContext), funcLikeDecl, genContext))) { return mlir::failure(); } } else if (item == SyntaxKind::MethodDeclaration || item == SyntaxKind::GetAccessor || item == SyntaxKind::SetAccessor) { auto funcLikeDecl = item.as(); - if (mlir::failed(processObjectFunctionLike(oli, funcLikeDecl, genContext))) { + auto fieldId = item == SyntaxKind::MethodDeclaration ? TupleFieldName(funcLikeDecl->name, genContext) : mlir::Attribute(); + if (mlir::failed(processObjectFunctionLike(oli, fieldId, funcLikeDecl, genContext))) { return mlir::failure(); } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 24769f69b..58dd6529e 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -608,6 +608,7 @@ tslang_add_test(NAME test-compile-00-class-accessor-2 COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-class-accessor-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") tslang_add_test(NAME test-compile-00-class-accessor-virtual COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts") tslang_add_test(NAME test-compile-00-class-accessor-implements-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_implements_field.ts") +tslang_add_test(NAME test-compile-00-object-literal-contextual-lambda COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") tslang_add_test(NAME test-compile-00-class-implicit-constructor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_implicit_constructor.ts") tslang_add_test(NAME test-compile-00-class-super-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") @@ -1222,6 +1223,7 @@ tslang_add_test(NAME test-jit-00-class-accessor-2 COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-class-accessor-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") tslang_add_test(NAME test-jit-00-class-accessor-virtual COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts") tslang_add_test(NAME test-jit-00-class-accessor-implements-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_implements_field.ts") +tslang_add_test(NAME test-jit-00-object-literal-contextual-lambda COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") tslang_add_test(NAME test-jit-00-class-implicit-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_implicit_constructor.ts") tslang_add_test(NAME test-jit-00-class-super-static COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") @@ -2175,6 +2177,7 @@ set(TSLANG_CORPUS 00class_accessor_super.ts 00class_accessor_virtual.ts 00class_accessor_implements_field.ts + 00object_literal_contextual_lambda.ts 00class_accessor.ts 00class_accessor2.ts 00class_constr_fields.ts diff --git a/tslang/test/tester/tests/00object_literal_contextual_lambda.ts b/tslang/test/tester/tests/00object_literal_contextual_lambda.ts new file mode 100644 index 000000000..2e07f1580 --- /dev/null +++ b/tslang/test/tester/tests/00object_literal_contextual_lambda.ts @@ -0,0 +1,63 @@ +// The untyped parameters of an object literal's function members take their types from the literal's +// receiver (docs/not_impl/not_impl28.ts): a typed variable or parameter, or, in a generic call, the +// parameter's type with its type parameters inferred first from the rest of the call - the other arguments, +// and the literal's other members (`read: () => s` gives T for `write: value => ...`). + +interface Computed { + read(): T; + write(value: T); +} + +function foo(x: Computed) { + x.write(x.read()); +} + +enum E1 { X } + +function f1(a: { w: (x: T) => U; r: () => T; }, b: T): U { + return a.w(b); +} + +interface Adder { + add(x: number): number; +} + +function useAdder(a: Adder) { + return a.add(2); +} + +function apply(o: { w: (x: number) => number }) { + return o.w(3); +} + +function main() { + let s: string = "a"; + + foo({ + read: () => s, + write: value => s = value + "!" + }); + assert(s == "a!"); + + foo({ + write: value => s = value + "?", + read: () => s + }); + assert(s == "a!?"); + + let v1: number = f1({ w: x => x, r: () => 0 }, 0); + assert(v1 == 0); + v1 = f1({ w: x => x + 1, r: () => 0 }, 5); + assert(v1 == 6); + + let v2: E1 = f1({ w: x => x, r: () => E1.X }, E1.X); + assert(v2 == E1.X); + + // not generic: the receiver's member types the parameters + const o: { w: (x: number) => number } = { w: x => x * 2 }; + assert(o.w(4) == 8); + assert(apply({ w: x => x + 1 }) == 4); + assert(useAdder({ add: x => x + 10 }) == 12); + + print("done."); +}