diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 1ff20b5f1..2ef6f1dfb 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -3527,9 +3527,18 @@ of a private virtual family. other than to be read or stored, an object of the literal's type made other than by the literal, a function-typed store through an open interface, or a class implementer (its slot is a thunk's cell) keeps the call open. In the language as it is, a function stored into such a field is - always the literal's: `s.f = fn` through the interface is `saving to constant object`, and a - function without `this` (a parameter, a named function) is held through a thunk, which is open - (`own_err_interface_fn_field_unknown`). + always the literal's: `s.f = fn` through the interface stops the program when a literal is behind + it, and a function without `this` (a parameter, a named function) is held through a thunk, which + is open (`own_err_interface_fn_field_unknown`). Through the interface such a store is taken as + unknown (its ref is also tested for null), so it opens every interface field's facts. +- **A class field written through an interface** (`h.f = show`, `h: Holder`, `Walker.f` filling + `Holder.f`): the class's cell names the field's offset (INTERFACE_FUNCTION_FIELD_WRITE_ATTR) and + the store goes to the instance. `collectFieldFamilies` sees no `ts.PropertyRef` at the store; the + family is kept sound by the cell's initializer, whose `ts.PropertyRef` of the field (its offset) is + neither a load nor a store, so the field's family is open whenever such a cell exists - also when + nothing stores through the interface. (Under §58 a class seen through an interface may keep its + field families; that cell then has to count as a store only when a write-marked + `ts.InterfaceSymbolRef` of the field's name exists in the module.) `own_interface_facts` runs the plan's loop over `sc.ts` (prints 9), a field borrowed across a call through two class members and an object-literal member, the closed forms of the five §27 negatives diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 3159b2acf..9d12e6fe5 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -207,18 +207,26 @@ // plus this bit. An access reads the cell (InterfaceSymbolRefOpLowering), then binds `this` as for // any function-typed field. The bit is free in every other function-typed slot: an offset of a // 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 function-typed interface field cannot be -// written ("saving to constant object"): a read binds `this`, so nothing stores to the cell. -// The cell is {method, flag offset}, the method first: a class's optional field holding a function -// (`fn?: () => int`, called through a thunk) has its flag at `this` + the offset, so a read of the -// interface's optional field (`fn?: () => int`) is absent while the instance's flag is clear; -1 -// is always present (a method, or a field that is not optional). +// is the cell; -1 (absent) is tested before it. +// The cell is {method, flag offset, field offset}, the method first: a class's optional field holding a +// function (`fn?: () => int`, called through a thunk) has its flag at `this` + the flag offset, so a read +// of the interface's optional field (`fn?: () => int`) is absent while the instance's flag is clear; -1 +// is always present (a method, or a field that is not optional). The field offset is of the class's +// field the thunk calls, when that field is stored as the interface field's function made a hybrid +// (`f: (v: T) => void` in both): a write through the interface (`h.f = g`) stores there. It is -1 +// where nothing can be written - a method, or a field of another type - and the write then stops the +// program (INTERFACE_FUNCTION_FIELD_WRITE_ATTR). // An 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; its cell is always present. +// 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 // 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" +// on an InterfaceSymbolRefOp of a function-typed field that is stored to (its result is then a ref to +// the field's function as a hybrid): the address of the class field a tagged slot's cell names, or +// null where the slot can't be written (a method's cell, or an object literal's field) +#define INTERFACE_FUNCTION_FIELD_WRITE_ATTR "__function_field_write" #define INSTANCEOF_NONE_NAME ".instanceOf.none" #define MAIN_ENTRY_NAME "main" #define TS_NEST_ATTRIBUTE "ts.nest" diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index 37a5963fb..39b3dd0ab 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -695,6 +695,16 @@ class MLIRTypeHelper return getFunctionTypeWithThisType(funcType, mlir_ts::OpaqueType::get(context), replace); } + // an interface's function-typed field (`f: (v: T) => void`, a function taking the object as an opaque + // `this`) as a class field of that type stores it: a hybrid of the function without the `this` + mlir_ts::HybridFunctionType getInterfaceFieldFunctionAsHybrid(mlir_ts::FunctionType fieldType) + { + auto offset = fieldType.getNumInputs() > 0 && isa(fieldType.getInput(0)) ? 1 : 0; + auto funcType = mlir_ts::FunctionType::get(context, fieldType.getInputs().slice(offset), fieldType.getResults(), + fieldType.isVarArg()); + return mlir_ts::HybridFunctionType::get(context, funcType); + } + mlir_ts::FunctionType getFunctionTypeAddingFirstArgType(mlir_ts::FunctionType funcType, mlir::Type firstArgType) { mlir::SmallVector funcArgTypes(funcType.getInputs().begin(), funcType.getInputs().end()); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index df75e3d22..b930f60be 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6802,6 +6802,45 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern( loc, th.getPtrType(), vtable, interfaceSymbolRefOp.getIndex() + INTERFACE_VTABLE_HEADER_SLOTS); + // a write of a function-typed field (`h.f = g`, not optional): the class field a tagged slot's cell + // names, or null where nothing can be written - a method's cell (field offset -1), or an untagged + // slot, an object literal's field holding a function that takes the object as its `this` + if (interfaceSymbolRefOp->hasAttr(INTERFACE_FUNCTION_FIELD_WRITE_ATTR)) + { + auto intPtrType = rewriter.getIntegerType(tsLlvmContext->compileOptions.sizeBits()); + auto slotValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); + auto tag = rewriter.create(loc, intPtrType, + rewriter.getIntegerAttr(intPtrType, INTERFACE_METHOD_CELL_TAG)); + auto tagBit = rewriter.create(loc, intPtrType, slotValue, tag); + auto zero = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, 0)); + auto isMethodCell = rewriter.create(loc, LLVM::ICmpPredicate::ne, tagBit, zero); + auto classFieldAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + // the cell is {method, flag offset, field offset} + 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(), intPtrType, intPtrType}, false); + auto fieldOffsetPtr = rewriter.create(loc, th.getPtrType(), cellLLVMType, cellAddr, + ArrayRef{0, 2}); + auto fieldOffset = rewriter.create(loc, intPtrType, fieldOffsetPtr); + auto negative1 = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, -1)); + auto isWritable = rewriter.create(loc, LLVM::ICmpPredicate::ne, fieldOffset, negative1); + auto thisInt = rewriter.create(loc, intPtrType, thisVal); + auto fieldInt = rewriter.create(loc, intPtrType, thisInt, fieldOffset); + auto fieldPtr = rewriter.create(loc, fieldLLVMTypeRef, fieldInt.getResult()); + auto nullAddr = rewriter.create(loc, fieldLLVMTypeRef); + return rewriter.create(loc, isWritable, fieldPtr, nullAddr); + }; + auto nullAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + return rewriter.create(loc, fieldLLVMTypeRef); + }; + + auto fieldAddr = clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, isMethodCell, classFieldAddrFunc, + nullAddrFunc); + rewriter.replaceOp(interfaceSymbolRefOp, ValueRange{fieldAddr}); + return success(); + } + if (auto boundFunc = dyn_cast(interfaceSymbolRefOp.getType())) { mlir::Value thisValEffective = thisVal; @@ -6909,9 +6948,9 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(loc, th.getPtrType(), cellLLVMType, cellAddr, ArrayRef{0, 1}); auto flagOffset = rewriter.create(loc, intPtrType, flagOffsetPtr); @@ -6994,7 +7033,7 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern int`): their cells carry the // offset of the field's flag, so an access reads the instance's presence llvm::SmallVector optionalFieldsForCells; + // 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; auto result = newInterfacePtr->getVirtualTable( virtualTable, [&](mlir::Attribute id, mlir::Type fieldType, bool isConditional) -> std::pair { @@ -2656,6 +2659,10 @@ genContext); { optionalFieldsForCells.push_back(id); } + else if (foundField.type == mth.getInterfaceFieldFunctionAsHybrid(funcType)) + { + writableFieldsForCells.push_back(id); + } } return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; @@ -2846,14 +2853,17 @@ genContext); return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; } - // the cell is {method, flag offset}, the method first so the slot is a ref to it. The offset - // is of the flag of an optional class field (`fn?: () => int`), from `this`; -1 is always - // present (a method, or a field that is not optional) + // the cell is {method, flag offset, field offset}, the method first so the slot is a ref to it. + // The flag offset is of the flag of an optional class field (`fn?: () => int`), from `this`; -1 + // is always present (a method, or a field that is not optional). The field offset is of the + // class field a write through the interface stores to; -1 is not written. auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); - mlir::Value flagOffsetValue; - if (llvm::is_contained(optionalFieldsForCells, id)) - { - // &(null)->field, stored as optional - {T, flag}, the flag right after T + auto negative1 = [&]() -> mlir::Value { + return builder.create(location, ptrIntType, builder.getIntegerAttr(ptrIntType, -1)); + }; + + // &(null)->field + auto fieldRefOf = [&]() -> mlir::Value { MLIRCodeLogic mcl(builder, compileOptions); auto nullObj = builder.create(location, getNullType()); auto classNull = cast(location, newClassPtr->classType, nullObj, genContext, true); @@ -2864,6 +2874,18 @@ genContext); emitError(location) << "can't find reference for field: " << id << " in interface: " << newInterfacePtr->fullName << " for class: " << newClassPtr->fullName; + } + + return fieldRef; + }; + + mlir::Value flagOffsetValue; + if (llvm::is_contained(optionalFieldsForCells, id)) + { + // stored as optional - {T, flag}, the flag right after T + auto fieldRef = fieldRefOf(); + if (!fieldRef) + { return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; } @@ -2879,14 +2901,31 @@ genContext); } else { - flagOffsetValue = - builder.create(location, ptrIntType, builder.getIntegerAttr(ptrIntType, -1)); + flagOffsetValue = negative1(); + } + + mlir::Value fieldOffsetValue; + if (llvm::is_contained(writableFieldsForCells, id)) + { + auto fieldRef = fieldRefOf(); + if (!fieldRef) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + fieldOffsetValue = builder.create(location, ptrIntType, fieldRef); + } + else + { + fieldOffsetValue = negative1(); } mlir::SmallVector cellFields{ {MLIRHelper::TupleFieldName("method", builder.getContext()), methodInfo.funcType, false, mlir_ts::AccessLevel::Public}, {MLIRHelper::TupleFieldName("flagOffset", builder.getContext()), ptrIntType, false, + mlir_ts::AccessLevel::Public}, + {MLIRHelper::TupleFieldName("fieldOffset", builder.getContext()), ptrIntType, false, mlir_ts::AccessLevel::Public}}; auto cellTupleType = getTupleType(cellFields); mlir::Value cellValue = builder.create(location, cellTupleType); @@ -2894,6 +2933,8 @@ genContext); MLIRHelper::getStructIndex(builder, 0)); cellValue = builder.create(location, cellTupleType, flagOffsetValue, cellValue, MLIRHelper::getStructIndex(builder, 1)); + cellValue = builder.create(location, cellTupleType, fieldOffsetValue, + cellValue, MLIRHelper::getStructIndex(builder, 2)); return TypeValueInitType{cellTupleType, cellValue, TypeProvided::Yes}; }, genContext); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 7a0df99a9..2e5423141 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -6676,6 +6676,72 @@ class MLIRGenImpl // } } + // `h.f` of an interface's function-typed field, read as the interface binds it to its object: the + // field's InterfaceSymbolRefOp, or null. An optional field (`fn?: () => int`) is bound only when present: + // `ts.If(ts.HasValue(v)) { ts.OptionalValue(ts.CreateBoundFunction(this, ts.Value(v))) }`. + mlir_ts::InterfaceSymbolRefOp interfaceFunctionFieldOf(mlir::Value value) + { + if (auto ifOp = value.getDefiningOp(); ifOp && !ifOp.getThenRegion().empty()) + { + auto resultOp = dyn_cast(ifOp.getThenRegion().front().getTerminator()); + auto index = mlir::cast(value).getResultNumber(); + auto optionalValueOp = resultOp && index < resultOp->getNumOperands() + ? resultOp->getOperand(index).getDefiningOp() + : mlir_ts::OptionalValueOp(); + value = optionalValueOp ? optionalValueOp.getIn() : mlir::Value(); + } + + auto boundOp = value ? value.getDefiningOp() : mlir_ts::CreateBoundFunctionOp(); + auto func = boundOp ? boundOp.getFunc() : mlir::Value(); + if (auto valueOp = func ? func.getDefiningOp() : mlir_ts::ValueOp()) + { + func = valueOp.getIn(); + } + + auto loadOp = func ? func.getDefiningOp() : mlir_ts::LoadOp(); + auto symbolRefOp = loadOp ? loadOp.getReference().getDefiningOp() + : mlir_ts::InterfaceSymbolRefOp(); + auto refType = symbolRefOp ? dyn_cast(symbolRefOp.getType()) : mlir_ts::RefType(); + return refType && isa(refType.getElementType()) ? symbolRefOp + : mlir_ts::InterfaceSymbolRefOp(); + } + + // `h.f = g` of an interface's function-typed field stores to the class field filling it, held there as + // the field's function made a hybrid (INTERFACE_FUNCTION_FIELD_WRITE_ATTR). Which object is behind the + // interface is known only at run time: one filling the field with a method, or an object literal, + // has nothing to store to, and the write stops the program. + ValueOrLogicalResult mlirGenSaveInterfaceFunctionField(mlir::Location location, + mlir_ts::InterfaceSymbolRefOp interfaceSymbolRefOp, + mlir::Value savingValue, const GenContext &genContext) + { + if (interfaceSymbolRefOp.getOptional().value_or(false)) + { + emitError(location, "an optional function-typed interface field can't be assigned through the " + "interface; assign it on the object"); + return mlir::failure(); + } + + auto fieldType = mlir::cast( + mlir::cast(interfaceSymbolRefOp.getType()).getElementType()); + auto hybridType = mth.getInterfaceFieldFunctionAsHybrid(fieldType); + auto writeOp = mlir::cast(builder.clone(*interfaceSymbolRefOp)); + writeOp.getResult().setType(mlir_ts::RefType::get(hybridType)); + writeOp->setAttr(INTERFACE_FUNCTION_FIELD_WRITE_ATTR, builder.getUnitAttr()); + + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto address = builder.create(location, ptrIntType, writeOp.getResult()); + auto zero = builder.create(location, ptrIntType, builder.getIntegerAttr(ptrIntType, 0)); + auto writable = builder.create( + location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::ExclamationEqualsToken), address, zero); + builder.create( + location, writable, mlir::Value(), + builder.getStringAttr("an interface's function field can't be assigned when the object fills it with a " + "method or is an object literal; only a class field holding the function can be")); + + auto fieldValue = builder.create(location, hybridType, writeOp.getResult()); + return mlirGenSaveLogicOneItem(location, fieldValue, savingValue, genContext); + } + ValueOrLogicalResult mlirGenSaveLogicOneItem(mlir::Location location, mlir::Value leftExpressionValue, mlir::Value rightExpressionValue, const GenContext &genContext) { @@ -6930,13 +6996,10 @@ class MLIRGenImpl boundIndirectIndexAccessorOp.getSetAccessor(), savingValue); } - /* - else if (auto createBoundFunction = leftExpressionValueBeforeCast.getDefiningOp()) + else if (auto interfaceSymbolRefOp = interfaceFunctionFieldOf(leftExpressionValueBeforeCast)) { - // TODO: i should not allow to change interface - return mlirGenSaveLogicOneItem(location, createBoundFunction.getFunc(), rightExpressionValue, genContext); + return mlirGenSaveInterfaceFunctionField(location, interfaceSymbolRefOp, savingValue, genContext); } - */ else if (auto lengthOf = leftExpressionValueBeforeCast.getDefiningOp()) { MLIRCodeLogic mcl(builder, compileOptions); diff --git a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp index 348dd32ed..af26354ba 100644 --- a/tslang/lib/TypeScript/MLIRGenInterfaces.cpp +++ b/tslang/lib/TypeScript/MLIRGenInterfaces.cpp @@ -821,22 +821,26 @@ namespace mlirgen auto thunkValue = builder.create( location, funcType, mlir::FlatSymbolRefAttr::get(builder.getContext(), thunkName)); - // the cell is {method, flag offset}, as a class's: the field holds a function, so the flag - // offset is -1, always present + // the cell is {method, flag offset, field offset}, as a class's: the field holds a function, + // so the flag offset is -1, always present, and the field offset is -1, not written through auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); - auto flagOffsetValue = + auto negative1 = builder.create(location, ptrIntType, builder.getIntegerAttr(ptrIntType, -1)); mlir::SmallVector cellFields{ {MLIRHelper::TupleFieldName("method", builder.getContext()), funcType, false, mlir_ts::AccessLevel::Public}, {MLIRHelper::TupleFieldName("flagOffset", builder.getContext()), ptrIntType, false, + mlir_ts::AccessLevel::Public}, + {MLIRHelper::TupleFieldName("fieldOffset", builder.getContext()), ptrIntType, false, mlir_ts::AccessLevel::Public}}; auto cellTupleType = getTupleType(cellFields); mlir::Value cellValue = builder.create(location, cellTupleType); cellValue = builder.create(location, cellTupleType, thunkValue, cellValue, MLIRHelper::getStructIndex(builder, 0)); - cellValue = builder.create(location, cellTupleType, flagOffsetValue, + cellValue = builder.create(location, cellTupleType, negative1, cellValue, MLIRHelper::getStructIndex(builder, 1)); + cellValue = builder.create(location, cellTupleType, negative1, + cellValue, MLIRHelper::getStructIndex(builder, 2)); return TypeValueInitType{cellTupleType, cellValue, TypeProvided::Yes}; }, genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index bc9fbdc81..739ed8ee1 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -665,6 +665,8 @@ 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-field-write COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_field_write.ts") +tslang_add_test(NAME test-compile-own-00-interface-function-field-write COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_field_write.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-class-function-typed-field-inferred COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_function_typed_field_inferred.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") @@ -1271,6 +1273,8 @@ 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-field-write COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_field_write.ts") +tslang_add_test(NAME test-jit-own-00-interface-function-field-write COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_function_field_write.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-class-function-typed-field-inferred COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_function_typed_field_inferred.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") @@ -2285,6 +2289,7 @@ set(TSLANG_CORPUS 00interface_function_typed_field.ts 00interface_function_typed_field_class.ts 00interface_function_typed_field_class_field.ts + 00interface_function_field_write.ts 00interface_function_typed_field_object_closure.ts 00class_function_typed_field_inferred.ts 00interface_generic.ts @@ -3054,14 +3059,14 @@ set_tests_properties(test-compile-diagnostic-location-rest-parameter-type PROPERTIES PASS_REGULAR_EXPRESSION "rest_parameter_type\\.ts:6:12: error: A rest parameter must be of an array type, not 's32'" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|rest_parameter_type\\.ts:1:|rest_parameter_type\\.ts:4:|verification") -# A function-typed interface field cannot be written: for a class its slot points at a method cell -# shared by every instance, so a write would change the function for all of them. +# An optional function-typed interface field cannot be written through the interface: the write would +# have to set the instance's presence flag as well. add_test(NAME test-compile-interface-function-field-write COMMAND $ --emit=obj --no-default-lib -mm=none "${PROJECT_SOURCE_DIR}/test/tester/interface-errors/function_field_write.ts" -o "${CMAKE_CURRENT_BINARY_DIR}/interface-function-field-write.obj") set_tests_properties(test-compile-interface-function-field-write - PROPERTIES PASS_REGULAR_EXPRESSION "function_field_write\\.ts:9:5: error: saving to constant object" + PROPERTIES PASS_REGULAR_EXPRESSION "function_field_write\\.ts:9:5: error: an optional function-typed interface field can't be assigned through the interface" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") # A call with fewer arguments than the callee's required parameters is an error (TS2554), @@ -5026,6 +5031,21 @@ foreach(array_length_name negative huge fraction negative_integer new_negative) endforeach() endforeach() +# A write through an interface to its function-typed field stops the program with a message when +# the object behind it fills the field with a method or is an object literal: only a class field +# holding the function has storage to write. Which object it is is known only at run time. +foreach(function_field_write_name method literal) + foreach(function_field_write_model gc rc none own) + add_test(NAME test-jit-${function_field_write_model}-interface-function-field-write-${function_field_write_name} + COMMAND $ --emit=jit --no-default-lib -mm=${function_field_write_model} + --shared-libs=$ + "${PROJECT_SOURCE_DIR}/test/tester/interface-function-field-write/${function_field_write_name}.ts") + set_tests_properties(test-jit-${function_field_write_model}-interface-function-field-write-${function_field_write_name} + PROPERTIES PASS_REGULAR_EXPRESSION "printed before.*assertion failed: an interface's function field can't be assigned when the object fills it with a method or is an object literal" + FAIL_REGULAR_EXPRESSION "Stack dump|PLEASE submit|not reached") + endforeach() +endforeach() + # --export=all exports the module's variables, not the locals of its functions. add_test(NAME test-compile-export-all-no-locals COMMAND $ --emit=mlir --no-default-lib --export=all diff --git a/tslang/test/tester/interface-errors/function_field_write.ts b/tslang/test/tester/interface-errors/function_field_write.ts index f3750f329..c9dbf2706 100644 --- a/tslang/test/tester/interface-errors/function_field_write.ts +++ b/tslang/test/tester/interface-errors/function_field_write.ts @@ -1,7 +1,7 @@ -// A function-typed interface field is read-only: a read binds the interface's `this`, and a -// write could not be checked against the object behind the interface. For a class the slot -// points at a method cell shared by every instance (INTERFACE_METHOD_CELL_TAG), which a write -// must not reach. +// An optional function-typed interface field is not written through the interface: a write would +// have to set the instance's presence flag too, which the class's cell names only for a read +// (INTERFACE_METHOD_CELL_TAG). A non-optional one filled by a class field is written +// (00interface_function_field_write.ts). interface MaybeFn { fn?: () => int; } class O { fn?: () => int; } function main() { diff --git a/tslang/test/tester/interface-function-field-write/literal.ts b/tslang/test/tester/interface-function-field-write/literal.ts new file mode 100644 index 000000000..20d92ef55 --- /dev/null +++ b/tslang/test/tester/interface-function-field-write/literal.ts @@ -0,0 +1,9 @@ +// An object literal filling an interface's function-typed field is not written through the +// interface: the write stops the program with a message. +interface Holder { f: () => int; } +function set(h: Holder) { h.f = () => 2; } +function main() { + print("printed before"); + set({ f() { return 1; } }); + print("not reached"); +} diff --git a/tslang/test/tester/interface-function-field-write/method.ts b/tslang/test/tester/interface-function-field-write/method.ts new file mode 100644 index 000000000..d0585b63b --- /dev/null +++ b/tslang/test/tester/interface-function-field-write/method.ts @@ -0,0 +1,10 @@ +// A class filling an interface's function-typed field with a method has no field to store to: a +// write through the interface stops the program with a message. +interface Holder { f: () => int; } +class WithMethod { f(): int { return 1; } } +function set(h: Holder) { h.f = () => 2; } +function main() { + print("printed before"); + set(new WithMethod()); + print("not reached"); +} diff --git a/tslang/test/tester/tests/00interface_function_field_write.ts b/tslang/test/tester/tests/00interface_function_field_write.ts new file mode 100644 index 000000000..7f1a268ac --- /dev/null +++ b/tslang/test/tester/tests/00interface_function_field_write.ts @@ -0,0 +1,78 @@ +// regression test: a write through an interface to its function-typed field (`h.f = show`) stores +// to the class field that fills it. The slot points at a cell holding a thunk that calls the field +// (INTERFACE_METHOD_CELL_TAG, Defines.h), and the cell names the field's offset, so the store goes +// to the instance behind the interface: "saving to constant object" before. + +class Item { + constructor(public n: number) {} +} + +interface Holder { + f: (v: Item) => void; +} + +class Walker { + constructor(public f: (v: Item) => void) {} +} + +let seen: number = 0; + +function show(v: Item) { + print(v.n); + seen += v.n; +} + +function twice(v: Item) { + seen += v.n * 2; +} + +function set(h: Holder) { + h.f = show; +} + +interface Counter { + next: () => int; +} + +class Steps { + next: () => int = () => 0; +} + +function count(c: Counter, by: int) { + let k = 0; + // a closure over a local, kept by the object after the call + c.next = () => (k += by); +} + +function main() { + const w = new Walker(twice); + set(w); + // read back on the instance itself: the write landed in the object + w.f(new Item(3)); + assert(seen == 3); + + // and through the interface + const h: Holder = w; + h.f(new Item(4)); + assert(seen == 7); + + h.f = twice; + w.f(new Item(5)); + assert(seen == 17); + + // each instance keeps its own + const other = new Walker(show); + const ho: Holder = other; + ho.f = twice; + w.f = show; + other.f(new Item(1)); + assert(seen == 19); + + const s = new Steps(); + count(s, 5); + assert(s.next() == 5); + const c: Counter = s; + assert(c.next() == 10); + + print("done."); +}