From 72f785783695c8fcb1fd59160baffa6d2089b85c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 00:24:46 +0100 Subject: [PATCH] A class's `v?: T` is `T | undefined`, directly and through an interface `class C { v?: string }` stored a plain string, so an unassigned `c.v` read as null (0 for a number) and `c.v === undefined` was false; the same for a constructor parameter field `constructor(public m?: string)`. Both are now optional, as a type literal's `v?: T` has been since #427. An interface's `v?: T` reads a field at an offset kept in the vtable, with the interface's T. A class can't have its vtable patched per object as a type literal does (an instance is shared, not copied), so its interface vtable marks such a slot with INTERFACE_OPTIONAL_STORAGE_TAG on top of the offset. InterfaceSymbolRefOpLowering then reads the field's flag: an unset one is undefined. A write through the interface (`i.v = x`) sets the flag, and `i.v = undefined` clears it: mlirGenSaveLogicOneItem marks the access it stores to with INTERFACE_OPTIONAL_WRITE_ATTR. InterfaceSymbolRefOp reads memory now (MemoryEffects<[MemRead]>, was Pure): a read of the flag before a write must not stand for one after it (CSE merged them, and LICM could hoist one out of a loop). DefaultLib's declarations must match its build: its RegExp classes declared fields `?` that the implementation stores plain (DefaultLib PR), and Error's `message?` now matches its `public message?` constructor fields. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 7 ++ tslang/include/TypeScript/TypeScriptOps.td | 4 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 44 ++++++- tslang/lib/TypeScript/MLIRGenClasses.cpp | 50 +++++++- tslang/lib/TypeScript/MLIRGenImpl.h | 21 +++- tslang/test/tester/CMakeLists.txt | 2 + .../tester/tests/00class_optional_fields.ts | 117 ++++++++++++++++++ 7 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 tslang/test/tester/tests/00class_optional_fields.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 66b21d01e..49e173edf 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -99,6 +99,13 @@ // INTERFACE_VTABLE_HEADER_SLOTS. #define INTERFACE_VTABLE_INSTANCEOF_SLOT 0 #define INTERFACE_VTABLE_HEADER_SLOTS 1 +// An interface `v?: T` field's slot is the field's offset from `this`, or -1 when absent. A class +// stores its `v?: T` as optional - {T, flag}, value first - and its slot carries this bit on top +// of the offset: an access then reads the flag (false is undefined), and a write sets it. +#define INTERFACE_OPTIONAL_STORAGE_TAG(sizeBits) (1LL << ((sizeBits) - 2)) +// 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" #define INSTANCEOF_NONE_NAME ".instanceOf.none" #define MAIN_ENTRY_NAME "main" #define TS_NEST_ATTRIBUTE "ts.nest" diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 9958dff85..b33e11a79 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -178,8 +178,10 @@ def BoundIndirectIndexAccessorOp : TypeScript_Op<"BoundIndirectIndexAccessor", let hasCanonicalizer = 1; } +// reads memory: the vtable slot, and for a `v?: T` field stored as optional its flag (see +// INTERFACE_OPTIONAL_STORAGE_TAG), so two accesses with a write between them are not one def InterfaceSymbolRefOp : TypeScript_Op<"InterfaceSymbolRef", - [Pure]> { + [MemoryEffects<[MemRead]>]> { let summary = "symbol reference as to interface member"; let arguments = (ins TypeScript_Interface:$interfaceVal, I32Attr:$index, AnyAttr:$identifier, OptionalAttr:$optional); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index aabbca330..dabadc032 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5768,14 +5768,54 @@ struct InterfaceSymbolRefOpLowering : public TsLlvmPatterncompileOptions.sizeBits()); + auto sizeBits = tsLlvmContext->compileOptions.sizeBits(); + auto intPtrType = rewriter.getIntegerType(sizeBits); auto negative1 = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, -1)); auto methodOrFieldIntPtrValue = rewriter.create(loc, intPtrType, methodOrFieldPtr); auto condVal = rewriter.create(loc, LLVM::ICmpPredicate::eq, methodOrFieldIntPtrValue, negative1); + // a slot tagged INTERFACE_OPTIONAL_STORAGE_TAG: the field is stored as optional (a class's `v?: T`), + // present only when its flag is set. A write through the interface sets the flag (clears it for + // `= undefined`), marked by MLIRGen with INTERFACE_OPTIONAL_WRITE_ATTR. + auto writeAttr = interfaceSymbolRefOp->getAttrOfType(INTERFACE_OPTIONAL_WRITE_ATTR); + auto storedAsOptionalAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + auto tag = rewriter.create( + loc, intPtrType, rewriter.getIntegerAttr(intPtrType, INTERFACE_OPTIONAL_STORAGE_TAG(sizeBits))); + auto offset = rewriter.create(loc, intPtrType, methodOrFieldIntPtrValue, tag); + auto thisInt = rewriter.create(loc, intPtrType, thisVal); + auto addrInt = rewriter.create(loc, intPtrType, thisInt, offset); + mlir::Value addr = rewriter.create(loc, fieldLLVMTypeRef, addrInt.getResult()); + + auto valueType = mlir::cast(interfaceSymbolRefOp.getType()).getElementType(); + auto optionalLLVMType = tch.convertType(mlir_ts::OptionalType::get(valueType)); + auto flagPtr = rewriter.create(loc, th.getPtrType(), optionalLLVMType, addr, + ArrayRef{0, OPTIONAL_HASVALUE_INDEX}); + if (writeAttr) + { + auto flagValue = rewriter.create(loc, rewriter.getI1Type(), + rewriter.getBoolAttr(writeAttr.getValue())); + rewriter.create(loc, flagValue, flagPtr); + return addr; + } + + auto hasValue = rewriter.create(loc, rewriter.getI1Type(), flagPtr); + auto nullAddr = rewriter.create(loc, fieldLLVMTypeRef); + return rewriter.create(loc, hasValue, addr, nullAddr); + }; + + auto presentAddrFunc = [&](OpBuilder &builder, Location location) -> mlir::Value { + auto tag = rewriter.create( + loc, intPtrType, rewriter.getIntegerAttr(intPtrType, INTERFACE_OPTIONAL_STORAGE_TAG(sizeBits))); + auto tagBit = rewriter.create(loc, intPtrType, methodOrFieldIntPtrValue, tag); + auto zero = rewriter.create(loc, intPtrType, rewriter.getIntegerAttr(intPtrType, 0)); + auto isStoredAsOptional = rewriter.create(loc, LLVM::ICmpPredicate::ne, tagBit, zero); + return clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, isStoredAsOptional, + storedAsOptionalAddrFunc, calcFieldTotalAddrFunc); + }; + auto result = - clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, condVal, nullAddrFunc, calcFieldTotalAddrFunc); + clh.conditionalExpressionLowering(loc, fieldLLVMTypeRef, condVal, nullAddrFunc, presentAddrFunc); fieldAddr = result; } else diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index d3dde59bc..f47474b73 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -813,9 +813,16 @@ namespace mlirgen type = mth.wideStorageType(type); } - LLVM_DEBUG(dbgs() << "\n!! class field: " << fieldId << " type: " << type << " access level: " << accessLevel); - auto hasType = !!propertyDeclaration->type; + + // `v?: T` is `v: T | undefined`, as in a type literal: unassigned it reads as undefined, not as T's zero. + // An interface's `v?: T` member reads it by its flag (see mlirGenClassVirtualTableDefinitionForInterface). + if (propertyDeclaration->questionToken && type && !mth.isNoneType(type)) + { + type = getUnionType(location, type, getUndefinedType()); + } + + LLVM_DEBUG(dbgs() << "\n!! class field: " << fieldId << " type: " << type << " access level: " << accessLevel); if (mth.isNoneType(type)) { if (hasType) @@ -1019,12 +1026,20 @@ namespace mlirgen auto [type, init, typeProvided] = evaluateTypeAndInit(parameter, genContext); - LLVM_DEBUG(dbgs() << "\n+++ class auto-gen field: " << fieldId << " type: " << type << ""); if (mth.isNoneType(type)) { return mlir::failure(); } + // `constructor(public m?: T)` is a field `m?: T`: T | undefined, as a declared one + // (mlirGenClassDataFieldMember) - `new Error().message` is undefined + if (parameter->questionToken) + { + type = getUnionType(location, type, getUndefinedType()); + } + + LLVM_DEBUG(dbgs() << "\n+++ class auto-gen field: " << fieldId << " type: " << type << ""); + fieldInfos.push_back( { fieldId, @@ -1607,12 +1622,18 @@ genContext); auto classStorageType = mlir::cast(newClassPtr->classType.getStorageType()); llvm::SmallVector virtualTable; + // the fields stored as optional for an interface's `v?: T`: their slots are tagged + llvm::SmallVector optionalStorageFields; auto result = newInterfacePtr->getVirtualTable( virtualTable, [&](mlir::Attribute id, mlir::Type fieldType, bool isConditional) -> std::pair { auto found = false; auto foundField = newClassPtr->findField(id, found); - if (!found || fieldType != foundField.type) + // a class's `v?: T` is stored as optional; the interface's `v?: T` reads it by its flag + // (a tagged slot, see isStoredAsOptional below) + auto optionalStorage = found && isConditional && isa(foundField.type) + && mlir::cast(foundField.type).getElementType() == fieldType; + if (!found || (fieldType != foundField.type && !optionalStorage)) { if (!found && !isConditional || found) { @@ -1626,6 +1647,11 @@ genContext); return {emptyFieldInfo, mlir::success()}; } + if (optionalStorage) + { + optionalStorageFields.push_back(id); + } + return {foundField, mlir::success()}; }, [&](std::string name, mlir_ts::FunctionType funcType, bool isConditional, int interfacePosIndex) -> std::pair { @@ -1798,6 +1824,22 @@ genContext); return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; } + // stored as optional - {T, flag}, value first - for an interface's `v?: T`: the slot is + // the offset plus INTERFACE_OPTIONAL_STORAGE_TAG, so an access reads the flag + // (InterfaceSymbolRefOpLowering) + if (llvm::is_contained(optionalStorageFields, methodOrField.fieldInfo.id)) + { + auto ptrIntType = builder.getIntegerType(compileOptions.sizeBits()); + auto offsetValue = builder.create(location, ptrIntType, fieldRef); + auto tagValue = builder.create( + location, ptrIntType, + builder.getIntegerAttr(ptrIntType, INTERFACE_OPTIONAL_STORAGE_TAG(compileOptions.sizeBits()))); + auto taggedValue = builder.create( + location, ptrIntType, builder.getI32IntegerAttr((int)SyntaxKind::PlusToken), offsetValue, + tagValue); + fieldRef = builder.create(location, fieldRef.getType(), taggedValue); + } + // insert &(null)->field vtableValue = builder.create( location, virtTuple, fieldRef, vtableValue, diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 05cba451b..7f660a3e4 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5500,6 +5500,21 @@ class MLIRGenImpl LLVM_DEBUG(llvm::dbgs() << "\n!! Dest type: " << destType << "\n";); + // an interface's `v?: T` field: a store sets its flag where it is stored as optional (a class's + // `v?: T`), and a store of undefined clears it - see INTERFACE_OPTIONAL_STORAGE_TAG + mlir::Value storeReference = loadOp.getReference(); + if (auto interfaceSymbolRefOp = storeReference.getDefiningOp()) + { + if (interfaceSymbolRefOp.getOptional().value_or(false) && isa(interfaceSymbolRefOp.getType())) + { + auto storesUndefined = isa(savingValue.getType()) + || (isa(savingValue.getType()) && isa_and_nonnull(savingValue.getDefiningOp())); + auto writeOp = mlir::cast(builder.clone(*interfaceSymbolRefOp)); + writeOp->setAttr(INTERFACE_OPTIONAL_WRITE_ATTR, builder.getBoolAttr(!storesUndefined)); + storeReference = writeOp.getResult(); + } + } + syncSavingValue(destType); if (!savingValue) { @@ -5512,7 +5527,7 @@ class MLIRGenImpl // stored back. Without this the release that eventually runs for this storage - // scope exit for a local, the instance's release routine for a field - would give // up a reference the assignment never took. - if (isOwningSlot(location, loadOp.getReference())) + if (isOwningSlot(location, storeReference)) { // `h.item = new C()` arrives already owned (ยง9.25), so the slot takes that // reference over instead of adding one. The release still runs either way - @@ -5527,12 +5542,12 @@ class MLIRGenImpl builder.create(location, savingValue); } - builder.create(location, loadOp.getReference()); + builder.create(location, storeReference); } // TODO: when saving const array into variable we need to allocate space and copy array as we need to have // writable array - auto storeOp = builder.create(location, savingValue, loadOp.getReference()); + auto storeOp = builder.create(location, savingValue, storeReference); cloneAtomicAttributes(loadOp, storeOp); } else if (auto extractPropertyOp = leftExpressionValueBeforeCast.getDefiningOp()) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9eb446104..e47889917 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -398,6 +398,7 @@ tslang_add_test(NAME test-compile-00-generator-return COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-return-inference-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_inference_undefined.ts") tslang_add_test(NAME test-compile-00-return-falls-off-end COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_falls_off_end.ts") tslang_add_test(NAME test-compile-00-nullish-values COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullish_values.ts") +tslang_add_test(NAME test-compile-00-class-optional-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_optional_fields.ts") tslang_add_test(NAME test-compile-00-signless-int-signed COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00signless_int_signed.ts") tslang_add_test(NAME test-compile-00-intersection-type COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type.ts") tslang_add_test(NAME test-compile-00-intersection-type-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type_generic.ts") @@ -880,6 +881,7 @@ tslang_add_test(NAME test-jit-00-generator-return COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-return-inference-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_inference_undefined.ts") tslang_add_test(NAME test-jit-00-return-falls-off-end COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00return_falls_off_end.ts") tslang_add_test(NAME test-jit-00-nullish-values COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullish_values.ts") +tslang_add_test(NAME test-jit-00-class-optional-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_optional_fields.ts") tslang_add_test(NAME test-jit-00-signless-int-signed COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00signless_int_signed.ts") tslang_add_test(NAME test-jit-00-union-global-init COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_global_init.ts") tslang_add_test(NAME test-jit-00-intersection-type COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type.ts") diff --git a/tslang/test/tester/tests/00class_optional_fields.ts b/tslang/test/tester/tests/00class_optional_fields.ts new file mode 100644 index 000000000..4ed69ae38 --- /dev/null +++ b/tslang/test/tester/tests/00class_optional_fields.ts @@ -0,0 +1,117 @@ +// a class's `v?: T` is `T | undefined`: unassigned it reads as undefined, directly and through an +// interface's `v?: T` (whose slot then reads the field's flag); a write through the interface sets it +interface I { + v?: string; + n?: number; + k: number; +} + +class C implements I { + v?: string; + n?: number; + constructor(public k: number) {} +} + +class Base { + a = 1; + v?: string; +} + +class D extends Base implements I { + k: number = 2; + x?: boolean = true; +} + +class G { + g?: T; +} + +interface J { + u?: number; +} + +class E implements J { + pad = "p"; + u?: number; +} + +class P { + constructor(public m?: string, public n?: number) {} +} + +type L = { u?: number }; + +function viaInterface(i: I) { + return i.n ?? -1; +} + +function sum(j: J) { + let s = 0; + for (let k = 0; k < 3; k++) { + if (j.u === undefined) { + j.u = 10; + } + + s += j.u ?? 100; + } + + return s; +} + +function viaLiteral(l: L) { + return l.u ?? -1; +} + +function main() { + const c = new C(1); + assert(c.v === undefined && c.n === undefined, "unassigned"); + assert((c.n ?? -1) == -1, "unassigned ??"); + assert(viaInterface(c) == -1, "unassigned through an interface"); + + c.n = 0; + assert(c.n === 0, "assigned"); + assert(viaInterface(c) == 0, "zero through an interface"); + + const i: I = c; + assert(i.v === undefined, "unassigned string through an interface"); + i.v = "x"; + assert(c.v == "x" && i.v == "x", "written through an interface"); + i.n = 5; + assert(c.n == 5 && i.n == 5, "number written through an interface"); + i.v = undefined; + assert(c.v === undefined && i.v === undefined, "undefined written through an interface"); + c.n = undefined; + assert(i.n === undefined, "undefined written directly"); + + const d = new D(); + const di: I = d; + assert(di.v === undefined, "inherited field"); + di.v = "dv"; + assert(d.v == "dv" && di.v == "dv", "inherited field written through an interface"); + assert(d.x === true, "initialized"); + d.x = undefined; + assert(d.x === undefined, "cleared"); + + const g = new G(); + assert(g.g === undefined, "generic"); + g.g = 3; + assert(g.g == 3, "generic assigned"); + + const e = new E(); + assert(sum(e) == 30 && e.u == 10, "loop through an interface"); + + const e2 = new E(); + assert(viaLiteral(e2) == -1, "to a type literal"); + e2.u = 4; + assert(viaLiteral(e2) == 4, "to a type literal, assigned"); + + const p = new P(); + assert(p.m === undefined && p.n === undefined, "constructor parameter fields"); + const p2 = new P("a", 0); + assert(p2.m == "a" && p2.n === 0, "constructor parameter fields, given"); + + const literal: I = { k: 3 }; + assert(literal.v === undefined, "object literal without the field"); + + print("done."); +}