From 5ade3e1d35e1aa28eded9e25b41439dbe3a0e1b8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 14:51:09 +0100 Subject: [PATCH 1/3] A class's optional function field fills an interface's optional one `class C { fn?: () => int }` seen as `interface I { fn?: () => int }` was a deliberate compile error: the slot points at a method cell shared by every instance, holding a thunk that calls `this.fn(...)`, so an unset field read as present and was called. Every method cell is now {method, flag offset}, the method first so the slot is still a ref to it. A method's (or a non-optional field's) offset is -1; an optional field's is the offset from `this` of its optional's flag. InterfaceSymbolRefOpLowering reads the offset on an optional read and gives a null address while the instance's flag is clear. A class's optional field still cannot fill a required interface field. The compile-error test is replaced by run tests (AOT, JIT, rc/none corpus): set, unset, assigned later, assigned undefined, inherited, a parameter, and a mixed array at one access site. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 4 + tslang/lib/TypeScript/LowerToLLVM.cpp | 43 ++++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 64 ++++++- tslang/test/tester/CMakeLists.txt | 14 +- .../optional_class_function_field.ts | 17 -- ...interface_optional_function_typed_field.ts | 2 +- ...tional_function_typed_field_class_field.ts | 168 ++++++++++++++++++ 7 files changed, 274 insertions(+), 38 deletions(-) delete mode 100644 tslang/test/tester/interface-errors/optional_class_function_field.ts create mode 100644 tslang/test/tester/tests/00interface_optional_function_typed_field_class_field.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index ae9c3f86e..d4548b687 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -166,6 +166,10 @@ // pointer-sized field (or of optional, which starts with one) is pointer-aligned, and so // is the cell; -1 (absent) is tested before it. A write through such a field changes the cell, // so it changes the method for every instance of the class seen through that interface. +// 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). #define INTERFACE_METHOD_CELL_TAG 1 // on an InterfaceSymbolRefOp of a `v?: T` field that is stored to: true sets the flag of a tagged slot's // field, false (a store of undefined) clears it diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 8ec50fb75..ff4653abb 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6478,10 +6478,13 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(interfaceSymbolRefOp.getType()); auto isFunctionField = fieldRefType && isa(fieldRefType.getElementType()); - auto orMethodCell = [&](std::function offsetAddrFunc) { - return [&, offsetAddrFunc](OpBuilder &builder, Location location) -> mlir::Value { + auto orMethodCell = [&](std::function offsetAddrFunc, bool checkPresence) { + return [&, offsetAddrFunc, checkPresence](OpBuilder &builder, Location location) -> mlir::Value { auto intPtrType = rewriter.getIntegerType(tsLlvmContext->compileOptions.sizeBits()); auto slotValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); auto tag = rewriter.create( @@ -6491,7 +6494,35 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(loc, LLVM::ICmpPredicate::ne, tagBit, zero); auto methodCellAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { auto cellInt = rewriter.create(loc, intPtrType, slotValue, tag); - return rewriter.create(loc, fieldLLVMTypeRef, cellInt.getResult()); + mlir::Value cellAddr = rewriter.create(loc, fieldLLVMTypeRef, cellInt.getResult()); + if (!checkPresence) + { + return cellAddr; + } + + // the cell is {method, flag offset} + auto cellLLVMType = + LLVM::LLVMStructType::getLiteral(rewriter.getContext(), {th.getPtrType(), intPtrType}, false); + auto flagOffsetPtr = rewriter.create(loc, th.getPtrType(), cellLLVMType, cellAddr, + ArrayRef{0, 1}); + auto flagOffset = rewriter.create(loc, intPtrType, flagOffsetPtr); + auto negative1 = + rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, -1)); + auto alwaysPresent = + rewriter.create(loc, LLVM::ICmpPredicate::eq, flagOffset, negative1); + auto cellAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { return cellAddr; }; + auto instanceFlagAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + auto thisInt = rewriter.create(loc, intPtrType, thisVal); + auto flagInt = rewriter.create(loc, intPtrType, thisInt, flagOffset); + auto flagPtr = rewriter.create(loc, th.getPtrType(), flagInt.getResult()); + auto hasValue = rewriter.create(loc, rewriter.getI1Type(), flagPtr); + auto nullAddr = rewriter.create(loc, fieldLLVMTypeRef); + return rewriter.create(loc, hasValue, cellAddr, nullAddr); + }; + + // the flag is loaded only when there is one + return clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, alwaysPresent, cellAddrFunc, + instanceFlagAddrFunc); }; return clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, isMethodCell, methodCellAddrFunc, @@ -6554,7 +6585,9 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(presentOrMethodCellAddrFunc) @@ -6563,7 +6596,7 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern int`) the class implements with a method // (`fn(): int`): their slots point at a cell holding the method (see INTERFACE_METHOD_CELL_TAG) llvm::SmallVector> methodsForFields; + // of those, the ones filled by an optional class field (`fn?: () => int`): their cells carry the + // offset of the field's flag, so an access reads the instance's presence + llvm::SmallVector optionalFieldsForCells; auto result = newInterfacePtr->getVirtualTable( virtualTable, [&](mlir::Attribute id, mlir::Type fieldType, bool isConditional) -> std::pair { @@ -2393,10 +2396,11 @@ genContext); // {function, `this` or null} - or, unannotated (`fn = () => 7`), a plain one: neither can be handed out // as a function taking the object as its `this`. A thunk calling `this.fn(...)` fills the // slot as a method does; it reads the field at each call, so a field assigned later is seen. - // Not an optional field (`fn?: () => int`): the cell is shared, so the slot would read as present - // on every instance, set or not; that stays "field type not matching" below. + // An optional field (`fn?: () => int`) fills only an optional one (`fn?: () => int`), and its cell + // carries the offset of the field's flag: the cell is shared, the flag is the instance's. + auto optionalField = isa(foundField.type); if (found && isa(fieldType) && fieldType != foundField.type - && !isa(foundField.type) && mth.isAnyFunctionType(foundField.type)) + && (!optionalField || isConditional) && mth.isAnyFunctionType(foundField.type)) { auto fieldName = dyn_cast(id); auto match = mth.TestFunctionTypesMatchWithObjectMethods(location, foundField.type, fieldType); @@ -2428,6 +2432,10 @@ genContext); thunkInfo.virtualIndex = -1; thunkInfo.accessLevel = mlir_ts::AccessLevel::Public; methodsForFields.push_back({id, thunkInfo}); + if (optionalField) + { + optionalFieldsForCells.push_back(id); + } } return {mlir_ts::FieldInfo{id, fieldType, false, mlir_ts::AccessLevel::Public}, mlir::success()}; @@ -2609,7 +2617,55 @@ genContext); return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; } - return TypeValueInitType{methodInfo.funcType, methodValue, TypeProvided::Yes}; + // 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) + 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 + MLIRCodeLogic mcl(builder, compileOptions); + auto nullObj = builder.create(location, getNullType()); + auto classNull = cast(location, newClassPtr->classType, nullObj, genContext, true); + auto fieldValue = mlirGenPropertyAccessExpression(location, classNull, id, genContext); + auto fieldRef = fieldValue ? mcl.GetReferenceFromValue(location, fieldValue) : mlir::Value(); + if (!fieldRef) + { + emitError(location) << "can't find reference for field: " << id + << " in interface: " << newInterfacePtr->fullName + << " for class: " << newClassPtr->fullName; + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + + auto optionalType = mlir::cast( + mlir::cast(fieldRef.getType()).getElementType()); + auto fieldOffset = builder.create(location, ptrIntType, fieldRef); + auto valueSize = builder.create(location, mth.getIndexType(), + optionalType.getElementType()); + auto valueSizeInt = builder.create(location, ptrIntType, valueSize); + flagOffsetValue = builder.create( + location, ptrIntType, builder.getI32IntegerAttr((int)SyntaxKind::PlusToken), fieldOffset, + valueSizeInt); + } + else + { + flagOffsetValue = + builder.create(location, ptrIntType, builder.getIntegerAttr(ptrIntType, -1)); + } + + 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}}; + auto cellTupleType = getTupleType(cellFields); + mlir::Value cellValue = builder.create(location, cellTupleType); + cellValue = builder.create(location, cellTupleType, methodValue, cellValue, + MLIRHelper::getStructIndex(builder, 0)); + cellValue = builder.create(location, cellTupleType, flagOffsetValue, + cellValue, MLIRHelper::getStructIndex(builder, 1)); + return TypeValueInitType{cellTupleType, cellValue, TypeProvided::Yes}; }, genContext); if (!cellType) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c90517699..76f9960cc 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -614,6 +614,7 @@ tslang_add_test(NAME test-compile-00-interface-function-typed-field COMMAND test 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-optional-function-typed-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field.ts") +tslang_add_test(NAME test-compile-00-interface-optional-function-typed-field-class-field COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field_class_field.ts") tslang_add_test(NAME test-compile-00-interface-captures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") tslang_add_test(NAME test-compile-00-object-annotated-method COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") tslang_add_test(NAME test-compile-00-object-annotated-method-params COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") @@ -1170,6 +1171,7 @@ tslang_add_test(NAME test-jit-00-interface-function-typed-field COMMAND test-run 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-optional-function-typed-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field.ts") +tslang_add_test(NAME test-jit-00-interface-optional-function-typed-field-class-field COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_optional_function_typed_field_class_field.ts") tslang_add_test(NAME test-jit-00-interface-captures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_captures.ts") tslang_add_test(NAME test-jit-00-object-annotated-method COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method.ts") tslang_add_test(NAME test-jit-00-object-annotated-method-params COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_annotated_method_params.ts") @@ -2046,6 +2048,7 @@ set(TSLANG_CORPUS 00interface_optional_cast_order.ts 00interface_optional_extends.ts 00interface_optional_function_typed_field.ts + 00interface_optional_function_typed_field_class_field.ts 00interface_optional_method_extends.ts 00interface_optional.ts 00interface_partial.ts @@ -2761,17 +2764,6 @@ 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 class's optional function-typed field cannot fill an interface's optional function-typed -# field yet: the method cell the slot would point at holds a thunk, present on every instance, -# so it is an error until the cell carries a per-instance presence check. -add_test(NAME test-compile-interface-optional-class-function-field - COMMAND $ --emit=obj --no-default-lib -mm=none - "${PROJECT_SOURCE_DIR}/test/tester/interface-errors/optional_class_function_field.ts" - -o "${CMAKE_CURRENT_BINARY_DIR}/interface-optional-class-function-field.obj") -set_tests_properties(test-compile-interface-optional-class-function-field - PROPERTIES PASS_REGULAR_EXPRESSION "field type not matching for \"fn\" for interface 'MaybeFn' in class 'OptField'" - FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") - # A call with fewer arguments than the callee's required parameters is an error (TS2554), # not padded with undef; so is one with more arguments than the callee has parameters # (too_many_*), which went silently into a slot the callee does not have (a rest parameter diff --git a/tslang/test/tester/interface-errors/optional_class_function_field.ts b/tslang/test/tester/interface-errors/optional_class_function_field.ts deleted file mode 100644 index e4a5d5d35..000000000 --- a/tslang/test/tester/interface-errors/optional_class_function_field.ts +++ /dev/null @@ -1,17 +0,0 @@ -// A class's OPTIONAL function-typed field (`fn?: () => int`) cannot fill an interface's -// optional function-typed field yet: the slot would point at the shared method cell, a -// thunk calling `this.fn(...)`, so the field would read as present on every instance - -// set or not - and an unset one would be called. Until the cell carries a per-instance -// presence check it is an error, as it was before class fields could fill such a slot. - -interface MaybeFn { - fn?: () => int; -} - -class OptField { - fn?: () => int; -} - -function main() { - const m: MaybeFn = new OptField(); -} diff --git a/tslang/test/tester/tests/00interface_optional_function_typed_field.ts b/tslang/test/tester/tests/00interface_optional_function_typed_field.ts index 3ca2e6a79..0a7e0da15 100644 --- a/tslang/test/tester/tests/00interface_optional_function_typed_field.ts +++ b/tslang/test/tester/tests/00interface_optional_function_typed_field.ts @@ -7,7 +7,7 @@ // (INTERFACE_METHOD_CELL_TAG, Defines.h) alike. // // A class's own optional function-typed field (`fn?: () => int`) filling the -// interface's is a compile error for now: interface-errors/optional_class_function_field.ts. +// interface's: 00interface_optional_function_typed_field_class_field.ts. interface MaybeFn { fn?: () => int; diff --git a/tslang/test/tester/tests/00interface_optional_function_typed_field_class_field.ts b/tslang/test/tester/tests/00interface_optional_function_typed_field_class_field.ts new file mode 100644 index 000000000..9aee5c4b3 --- /dev/null +++ b/tslang/test/tester/tests/00interface_optional_function_typed_field_class_field.ts @@ -0,0 +1,168 @@ +// regression test: a class's OPTIONAL function-typed field (`fn?: () => int`) +// filling an interface's optional function-typed field. The interface slot +// points at a method cell shared by every instance of the class, holding a +// thunk that calls `this.fn(...)` (INTERFACE_METHOD_CELL_TAG, Defines.h); the +// cell also carries the offset of the field's presence flag, so each instance +// reads as present only while its own field is set. It was a compile error +// ("field type not matching"): without the flag an unset field read as present +// and the thunk called it. + +interface MaybeFn { + fn?: () => int; +} + +function maybe(m: MaybeFn): int { + const g = m.fn; + if (g) { + return g(); + } + + return -1; +} + +function callOpt(m: MaybeFn): int { + return m.fn?.() ?? -2; +} + +function isSet(m: MaybeFn): boolean { + return m.fn !== undefined; +} + +class OptField { + fn?: () => int; + constructor(public v: int, set: boolean) { + if (set) { + this.fn = () => this.v; + } + } +} + +// the flag sits after a field of another size +class OptFieldAfter { + a = 1; + fn?: () => int; + b = 2; +} + +class WithFn { + constructor(public v: int) {} + fn(): int { + return this.v; + } +} + +class WithoutFn { + constructor(public v: int) {} +} + +interface MaybeAdd { + base: int; + add?: (x: int) => int; +} + +function addOr(m: MaybeAdd, x: int): int { + const add = m.add; + if (add) { + return add(x); + } + + return m.base; +} + +class OptAdder { + add?: (x: int) => int; + constructor(public base: int) {} +} + +// the field is inherited +class OptFieldDerived extends OptField { + constructor(v: int, set: boolean) { + super(v, set); + } +} + +function main() { + // set, unset; under -mm=rc a bound value does not hold its receiver yet, so each + // instance is kept in a local while it is used through the interface + const s21 = new OptField(21, true); + const u22 = new OptField(22, false); + print(maybe(s21), maybe(u22)); + assert(maybe(s21) == 21); + assert(maybe(u22) == -1); + assert(callOpt(s21) == 21); + assert(callOpt(u22) == -2); + assert(isSet(s21)); + assert(!isSet(u22)); + + // assigned later, through the class + const o = new OptField(1, false); + const m: MaybeFn = o; + print(maybe(m)); + assert(maybe(m) == -1); + o.fn = () => 9; + print(maybe(m)); + assert(maybe(m) == 9); + print(m.fn?.() ?? -2); + assert((m.fn?.() ?? -2) == 9); + assert(isSet(m)); + + // assigned undefined again + o.fn = undefined; + assert(maybe(m) == -1); + assert(callOpt(m) == -2); + assert(!isSet(m)); + + // the cell is shared: setting one instance's field leaves another unset + const p = new OptField(2, false); + const q = new OptField(3, false); + p.fn = () => 30; + assert(maybe(p) == 30); + assert(maybe(q) == -1); + q.fn = () => p.v + q.v; + assert(maybe(p) == 30); + assert(maybe(q) == 5); + + // the field after another one + const after = new OptFieldAfter(); + assert(maybe(after) == -1); + after.fn = () => after.a + after.b; + assert(maybe(after) == 3); + assert(after.a == 1 && after.b == 2); + + // with a parameter + const adder = new OptAdder(10); + assert(addOr(adder, 5) == 10); + adder.add = (x: int) => adder.base + x; + assert(addOr(adder, 5) == 15); + + // inherited + const d1 = new OptFieldDerived(41, true); + const d2 = new OptFieldDerived(42, false); + assert(maybe(d1) == 41); + assert(maybe(d2) == -1); + + // one access site, mixed implementers + const set4 = new OptField(4, true); + const unset5 = new OptField(5, false); + const with6 = new WithFn(6); + const without7 = new WithoutFn(7); + const all: MaybeFn[] = [set4, unset5, with6, without7, { fn() { return 8; } }, {}]; + let sum = 0; + for (const a of all) { + sum += maybe(a); + } + + print(sum); + assert(sum == 4 - 1 + 6 - 1 + 8 - 1); + + unset5.fn = () => 50; + set4.fn = undefined; + sum = 0; + for (const a of all) { + sum += callOpt(a); + } + + assert(sum == -2 + 50 + 6 - 2 + 8 - 2); + + print("done."); +} From 9670d5ae202e51b988890cbfa1670266f90e3010 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 15:17:05 +0100 Subject: [PATCH 2/3] A function-typed interface field stays read-only Writing one (`m.fn = f`) is "saving to constant object" for every implementer: a read binds the interface's `this`, and a write could not be checked against the object behind the interface. The comments claiming a write changes a class's shared method cell are corrected, the lowering's presence check no longer looks for a write, and a test pins the error for the cell case (a class's optional function field). Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 4 ++-- tslang/lib/TypeScript/LowerToLLVM.cpp | 6 +++--- tslang/test/tester/CMakeLists.txt | 10 ++++++++++ .../tester/interface-errors/function_field_write.ts | 10 ++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/interface-errors/function_field_write.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index d4548b687..9964d4aec 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -164,8 +164,8 @@ // 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 write through such a field changes the cell, -// so it changes the method for every instance of the class seen through that interface. +// 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 diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index ff4653abb..22f87177b 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -6585,9 +6585,9 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPattern(presentOrMethodCellAddrFunc) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 76f9960cc..3f704644b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2764,6 +2764,16 @@ 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. +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" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") + # A call with fewer arguments than the callee's required parameters is an error (TS2554), # not padded with undef; so is one with more arguments than the callee has parameters # (too_many_*), which went silently into a slot the callee does not have (a rest parameter diff --git a/tslang/test/tester/interface-errors/function_field_write.ts b/tslang/test/tester/interface-errors/function_field_write.ts new file mode 100644 index 000000000..f3750f329 --- /dev/null +++ b/tslang/test/tester/interface-errors/function_field_write.ts @@ -0,0 +1,10 @@ +// 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. +interface MaybeFn { fn?: () => int; } +class O { fn?: () => int; } +function main() { + const m: MaybeFn = new O(); + m.fn = () => 9; +} From 8efbba2596d51a5d9c3d519e4ca82669643f6864 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 19 Oct 2026 16:06:13 +0100 Subject: [PATCH 3/3] Test a class field's optional type only when the class has the field The guard read isa of the found field's type before checking the field was found. For a class lacking the field (`class WithoutFn`) the type is null and the cast dereferences it: the compiler segfaulted on Linux (found by ASan), while the MSVC build happened to survive. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 55b915d49..3d7136862 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2398,7 +2398,7 @@ genContext); // slot as a method does; it reads the field at each call, so a field assigned later is seen. // An optional field (`fn?: () => int`) fills only an optional one (`fn?: () => int`), and its cell // carries the offset of the field's flag: the cell is shared, the flag is the instance's. - auto optionalField = isa(foundField.type); + auto optionalField = found && isa(foundField.type); if (found && isa(fieldType) && fieldType != foundField.type && (!optionalField || isConditional) && mth.isAnyFunctionType(foundField.type)) {