From 98e92da1fdc7236bbee8e4bb0678e4c999aa803d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 11:55:29 +0000 Subject: [PATCH 1/3] @value T | null: ?: of a literal holding a new object (rc), ??, and print Three failures of a @value type that may be missing, all on main before #760: - rc: `d < 0 ? null : { at: d, owner: new Owner(d) }` returned as `Mark | null`. The branch released the new object before the result's retain after the join, which then retained freed memory. consumeConditionalResults' producerOf looks through a tuple cast to the @value type it is laid out as, so yieldsLiteralOfBranchTemporary sees the literal and the branch retains first. - `h ?? find(3)` was typed `Hit | (Hit | null)` and did not compile: an optional member of a union gave its element as one member. processUnionTypeItem now processes the element like any other member, so a union in it gives its members. - print / string conversion: an undefined `T | undefined` printed as null or as a zeroed T (the optional went through ValueOrDefault); it is "undefined" now. A null `T | null` stored without a tag (#760) printed T's fields; castNullableToString takes a @value base too and prints "null". Test value/value_missing_ops (gc, rc, none; own cannot yet tell which branch of a `?:` owns a literal it merges). ctest 6546/6546; fuzz seeds 83000-83599 clean; DefaultLib compiles under own and rc. Printing a @value value holding an interface still crashes, as printing any interface does - not changed here. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 5 +- tslang/lib/TypeScript/MLIRGenCast.cpp | 23 ++++-- .../TypeScript/OwnedReturnConsumptionPass.cpp | 8 +- tslang/test/tester/CMakeLists.txt | 6 ++ tslang/test/tester/value/value_missing_ops.ts | 73 +++++++++++++++++++ 5 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 tslang/test/tester/value/value_missing_ops.ts diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index 6b191a21c..72de7b9d9 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -3551,7 +3551,10 @@ class MLIRTypeHelper { if (unionContext.isUndefined) { - unionContext.types.insert(optionalType.getElementType()); + // its element as a member like any other: a union in it (`Hit | null`, the optional a + // @value type's `T | null` is) gives its members, not itself as one - `h ?? find(3)` + // was typed `Hit | (Hit | null)`, and failed + return processUnionTypeItem(optionalType.getElementType(), unionContext); } else { diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index c6404a11a..9e01498e3 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -2002,8 +2002,10 @@ namespace mlirgen return castFromUnion(location, type, value, genContext); } + // ...and a @value type's `T | null` stored as T (MLIRTypeHelper::getValueNullNiche): null + // is "null", not T's fields read from a zeroed T if (isa(type) - && isa(baseType) + && isa(baseType) && llvm::any_of(unionType.getTypes(), [](auto subType) { return isa(subType); })) { return castNullableToString(location, type, value, baseType, genContext); @@ -2027,7 +2029,11 @@ namespace mlirgen // unwrapping optional value to work with union inside, we need it as ' | undefined ' is part of union type if (auto optType = dyn_cast(valueType)) { - if (isa(optType.getElementType())) + // ...but not a @value type's that may be missing put into a string: undefined is "undefined", + // not the default of its union (a zeroed value, read as null or as T's fields) - below + auto missingValueToString = + isa(type) && MLIRTypeHelper::getMissingValueElement(optType); + if (isa(optType.getElementType()) && !missingValueToString) { auto val = V(builder.create(location, optType.getElementType(), value)); CAST_A(unwrappedValue, location, type, val, genContext); @@ -3235,11 +3241,18 @@ namespace mlirgen // see nullStringAsText in LowerToLLVM.) ValueOrLogicalResult MLIRGenImpl::castNullableToString(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type baseType, const GenContext &genContext) { - CAST_A(opaqueValue, location, getOpaqueType(), value, genContext); + // a class is compared as its pointer; a @value type's union as itself, whose null is a pointer + // in it (the lowering's valueNullCompare) + mlir::Value compared = value; + if (!isa(baseType)) + { + CAST_A(opaqueValue, location, getOpaqueType(), value, genContext); + compared = opaqueValue; + } auto nullVal = builder.create(location, getNullType()); auto isNull = builder.create( - location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), opaqueValue, + location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), compared, nullVal); auto ifOp = builder.create(location, mlir::TypeRange{type}, isNull, true); @@ -3249,7 +3262,7 @@ namespace mlirgen builder.create(location, mlir::ValueRange{nullText}); builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); - // the union as the class: the same pointer, now known not to be null + // the union as its base type, now known not to be null: the same pointer, or the same value auto baseValue = V(builder.create(location, baseType, value)); CAST_A(text, location, type, baseValue, genContext); builder.create(location, mlir::ValueRange{text}); diff --git a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp index 5ef623697..88cb0f42a 100644 --- a/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp +++ b/tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp @@ -1576,7 +1576,9 @@ class OwnedReturnConsumptionPass // branch borrows, the receiver's retain is already the right and only answer. // What a branch yields, seen through the casts that only widen it: `new C()` yielded as // `C | null` or `C | undefined` is still that block, so its reference goes out with it - // (#465). A cast that makes a new value (a box) carries its own reference instead. + // (#465). So is a literal yielded as the @value type it is laid out as (the cast lowers to + // nothing): `c ? null : { at: d, owner: new Owner(d) }` of `Mark | null`. A cast that makes + // a new value (a box) carries its own reference instead. auto producerOf = [](mlir::Value value) { while (auto *definingOp = value.getDefiningOp()) { @@ -1586,7 +1588,9 @@ class OwnedReturnConsumptionPass } if (auto castOp = mlir::dyn_cast(definingOp); - castOp && mlir::isa(castOp.getType())) + castOp && (mlir::isa(castOp.getType()) || + (mlir::isa(castOp.getType()) && + mlir::isa(castOp.getIn().getType())))) { value = castOp.getIn(); } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7cdae37df..d4fe2abf8 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3392,6 +3392,12 @@ foreach(value_mm gc rc none) tslang_add_test(NAME test-compile-value-missing-niche-${value_mm} COMMAND test-runner -mm=${value_mm} "${PROJECT_SOURCE_DIR}/test/tester/value/value_missing_niche.ts") tslang_add_test(NAME test-jit-value-missing-niche-${value_mm} COMMAND test-runner -jit -mm=${value_mm} "${PROJECT_SOURCE_DIR}/test/tester/value/value_missing_niche.ts") endforeach() +# ...made by `?:` from null and a literal holding a new object, given to `??`, and printed. Not under own, +# which cannot yet tell which branch of a `?:` owns a literal it merges. +foreach(value_mm gc rc none) + tslang_add_test(NAME test-compile-value-missing-ops-${value_mm} COMMAND test-runner -mm=${value_mm} "${PROJECT_SOURCE_DIR}/test/tester/value/value_missing_ops.ts") + tslang_add_test(NAME test-jit-value-missing-ops-${value_mm} COMMAND test-runner -jit -mm=${value_mm} "${PROJECT_SOURCE_DIR}/test/tester/value/value_missing_ops.ts") +endforeach() # ...and `g?.ray.dir.x` (and `.len()`) of a missing one skips the rest of the chain: undefined, as is a # type literal's foreach(value_mm gc rc none own) diff --git a/tslang/test/tester/value/value_missing_ops.ts b/tslang/test/tester/value/value_missing_ops.ts new file mode 100644 index 000000000..23cb0bce1 --- /dev/null +++ b/tslang/test/tester/value/value_missing_ops.ts @@ -0,0 +1,73 @@ +// @strict-null false +// A @value type that may be missing (`T | null`, `T | undefined`), in three places that failed: +// - made by `?:` from null and a literal holding a new object: under rc the branch released the +// object before the result kept it, and the program read freed memory; +// - `??`: its type was `T | (T | null)`, and the program did not compile; +// - printed: undefined printed as null or as a zeroed T, and a null `T | null` whose T holds a class +// (no tag, MLIRTypeHelper::getValueNullNiche) printed T's fields instead of null. +// Each runs for a T holding a class (no tag) and a T of numbers only (tagged). +class Owner { + constructor(public id: number) {} + + toString() { + return "Owner" + this.id; + } +} + +@value +interface Mark { + readonly at: number; + readonly owner: Owner; +} + +@value +interface Num { + readonly at: number; +} + +function churn() { + const junk: Owner[] = []; + for (let i = 0; i < 64; i++) junk.push(new Owner(999)); +} + +function mark(d: number): Mark | null { + return d < 0 ? null : { at: d, owner: new Owner(d) }; +} + +function num(d: number): Num | null { + return d < 0 ? null : { at: d }; +} + +function main() { + // ?: of null and a literal holding a new object + const m = mark(4); + churn(); + assert(m.owner.id == 4 && m.at == 4, "the object the literal holds outlives the branch"); + assert(mark(-1) === null && num(-1) === null && num(3).at == 3); + let total: number = 0; + for (let i = 0; i < 1000; i++) { + const k = mark(i % 3 - 1); + if (k != null) total += k.owner.id; + } + assert(total == 333, "made in a loop"); + + // ?? + assert((mark(-1) ?? mark(3)).owner.id == 3 && (mark(2) ?? mark(3)).owner.id == 2); + const fallback: Mark = { at: 7, owner: new Owner(7) }; + assert((mark(-1) ?? fallback).at == 7); + assert((num(-1) ?? num(4)).at == 4 && (num(5) ?? { at: 6 }).at == 5); + const nothing: Mark | undefined = undefined; + assert((nothing ?? fallback).at == 7); + + // printed + assert(`${mark(-1)}` == "null", "null of a T holding a class"); + assert(`${mark(2)}` == "{ at: 2, owner: Owner2 }"); + assert(`${num(-1)}` == "null" && `${num(5)}` == "{ at: 5 }"); + const u: Mark | undefined = undefined; + const v: Num | undefined = undefined; + const n: Mark | null | undefined = null; + assert(`${u}` == "undefined" && `${v}` == "undefined" && `${n}` == "null", "undefined and null apart"); + print(mark(-1), mark(2), u, v); + + print("done."); +} From 7fa8c156f9d3b56aac33f813c2fabb607a0b3bda Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 12:18:57 +0000 Subject: [PATCH 2/3] An interface printed shows its fields, as an object literal does Printing an interface value, or putting it into a string, crashed at run time, whatever object was behind it - a class, an object literal, or one a @value value holds. USER RULING: print its fields. castInterfaceToString: - its `toString` when the interface has one, as a class's is; - else its fields, read through the interface, those of the interfaces it extends first, in an object literal's format (`{ n: 3, name: 'b' }`, fieldsToString, which castTupleToString now shares); - null as "null"; - a field of an interface type already being printed is `[Object]`: the text is made inline, and `interface Chain { next: Chain }` nests without end. Test tests/00interface_print (every model). ctest 6552/6552; fuzz seeds 84000-84599 clean; DefaultLib compiles under all four models. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenCast.cpp | 119 ++++++++++++++++-- tslang/lib/TypeScript/MLIRGenImpl.h | 11 +- tslang/test/tester/CMakeLists.txt | 3 + tslang/test/tester/tests/00interface_print.ts | 73 +++++++++++ 4 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 tslang/test/tester/tests/00interface_print.ts diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 9e01498e3..c4025fe17 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -497,6 +497,24 @@ namespace mlirgen ValueOrLogicalResult MLIRGenImpl::castTupleToString(mlir::Location location, mlir::Value value, mlir_ts::TupleType tupleType, ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, const GenContext &genContext) + { + SmallVector> fieldValues; + for (auto [index, fieldInfo] : enumerate(fields)) + { + LLVM_DEBUG(llvm::dbgs() << "\n!! processing #" << index << " field [" << fieldInfo.id << "]\n";); + + MLIRPropertyAccessCodeLogic cl(compileOptions, builder, location, value, builder.getI32IntegerAttr(index)); + auto fieldValue = cl.Tuple(tupleType, true); + VALIDATE(value, location) + fieldValues.push_back({fieldInfo.id, fieldValue}); + } + + return fieldsToString(location, fieldValues, genContext); + } + + // `{ a: 1, b: 'text' }`: each field's name and its value as a string, a string's in quotes + ValueOrLogicalResult MLIRGenImpl::fieldsToString(mlir::Location location, + ::llvm::ArrayRef> fieldValues, const GenContext &genContext) { auto stringType = getStringType(); SmallVector strs; @@ -521,28 +539,24 @@ namespace mlirgen strs.push_back(beginValue); - for (auto [index, fieldInfo] : enumerate(fields)) + for (auto [index, field] : enumerate(fieldValues)) { - LLVM_DEBUG(llvm::dbgs() << "\n!! processing #" << index << " field [" << fieldInfo.id << "]\n";); - - if (index > 0) + auto fieldId = field.first; + auto fieldValue = field.second; + if (index > 0) { // text strs.push_back(spanValue); } strs.push_back(spaceValue); - if (fieldInfo.id) + if (fieldId) { - auto fieldNameValue = builder.create(location, stringType, fieldInfo.id); + auto fieldNameValue = builder.create(location, stringType, fieldId); strs.push_back(fieldNameValue); strs.push_back(fieldSepValue); } - MLIRPropertyAccessCodeLogic cl(compileOptions, builder, location, value, builder.getI32IntegerAttr(index)); - auto fieldValue = cl.Tuple(tupleType, true); - VALIDATE(value, location) - if (fieldValue.getType() != stringType) { CAST(fieldValue, location, stringType, fieldValue, genContext); @@ -576,6 +590,86 @@ namespace mlirgen return V(concatValues); } + // An interface printed: its `toString` when it has one, as a class's is; else its fields, read + // through it, as an object literal's are (`{ n: 3 }`), those of the interfaces it extends first; + // and null as "null". An interface whose fields hold its own type again is printed `[Object]` + // there: the text is made inline, and the type nests without end. + ValueOrLogicalResult MLIRGenImpl::castInterfaceToString(mlir::Location location, mlir::Value value, + mlir_ts::InterfaceType interfaceType, const GenContext &genContext) + { + auto stringType = getStringType(); + auto interfaceInfo = getInterfaceInfoByFullName(interfaceType.getName().getValue()); + if (!interfaceInfo) + { + return mlir::failure(); + } + + if (llvm::is_contained(interfacesBeingPrinted, interfaceType)) + { + return V(builder.create(location, stringType, getStringAttr("[Object]"))); + } + + interfacesBeingPrinted.push_back(interfaceType); + auto popPrinted = llvm::make_scope_exit([&]() { interfacesBeingPrinted.pop_back(); }); + + auto nullValue = builder.create(location, getNullType()); + auto isNull = builder.create( + location, getBooleanType(), builder.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), value, nullValue); + auto ifOp = builder.create(location, mlir::TypeRange{stringType}, isNull, true); + + builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); + builder.create(location, mlir::ValueRange{ + builder.create(location, stringType, builder.getStringAttr(NULL_NAME))}); + + builder.setInsertionPointToStart(&ifOp.getElseRegion().front()); + mlir::Value text; + if (interfaceInfo->findMethod(TO_STRING)) + { + auto result = mlirGenCallThisMethod(location, value, TO_STRING, undefined, undefined, genContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + text = V(result); + if (text.getType() != stringType) + { + CAST(text, location, stringType, text, genContext); + } + } + else + { + llvm::SmallVector fields; + std::function collect = [&](InterfaceInfo::TypePtr info) { + for (auto &extent : info->extends) + { + collect(std::get<1>(extent)); + } + + fields.append(info->fields.begin(), info->fields.end()); + }; + collect(interfaceInfo); + + SmallVector> fieldValues; + for (auto &field : fields) + { + auto name = mlir::dyn_cast_or_null(field.id); + if (!name) + { + continue; + } + + auto fieldValue = mlirGenPropertyAccessExpression(location, value, name.getValue(), genContext); + EXIT_IF_FAILED_OR_NO_VALUE(fieldValue) + fieldValues.push_back({field.id, V(fieldValue)}); + } + + auto result = fieldsToString(location, fieldValues, genContext); + EXIT_IF_FAILED_OR_NO_VALUE(result) + text = V(result); + } + + builder.create(location, mlir::ValueRange{text}); + builder.setInsertionPointAfter(ifOp); + return V(ifOp.getResults().front()); + } + ValueOrLogicalResult MLIRGenImpl::generatingStaticNewCtorForClass(mlir::Location location, ClassInfo::TypePtr classInfo, int posIndex, const GenContext &genContext) { if (auto classConstrMethodInfo = classInfo->findMethod(CONSTRUCTOR_NAME)) @@ -1071,6 +1165,11 @@ namespace mlirgen return mlirGenCallThisMethod(location, value, TO_STRING, undefined, undefined, genContext); } + if (auto interfaceType = dyn_cast(valueType)) + { + return castInterfaceToString(location, value, interfaceType, genContext); + } + if (auto arrayType = dyn_cast(valueType)) { return castConstArrayToString(location, value, genContext); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index dd5e8f3c3..3206dffa7 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -14921,7 +14921,13 @@ class MLIRGenImpl ValueOrLogicalResult castConstArrayToString(mlir::Location location, mlir::Value value, const GenContext &genContext); ValueOrLogicalResult castTupleToString(mlir::Location location, mlir::Value value, mlir_ts::TupleType tupleType, - ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, const GenContext &genContext); + ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, const GenContext &genContext); + + ValueOrLogicalResult fieldsToString(mlir::Location location, + ::llvm::ArrayRef> fieldValues, const GenContext &genContext); + + ValueOrLogicalResult castInterfaceToString(mlir::Location location, mlir::Value value, + mlir_ts::InterfaceType interfaceType, const GenContext &genContext); ValueOrLogicalResult generatingStaticNewCtorForClass(mlir::Location location, ClassInfo::TypePtr classInfo, int posIndex, const GenContext &genContext); @@ -16902,6 +16908,9 @@ class MLIRGenImpl // every class this module made, its imported sources' too (devirtualizeClosedMethods) llvm::SmallVector moduleClasses; + // the interfaces whose text castInterfaceToString is making, outermost first + llvm::SmallVector interfacesBeingPrinted; + // the vtables of classes for interfaces they don't declare that casts ask for // (interfaceVTableNameForClass), from the discovery pass on: see castAnyToInterface llvm::StringSet<> classInterfaceVTablesCast; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index d4fe2abf8..812e73da4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -615,6 +615,7 @@ tslang_add_test(NAME test-compile-00-object-literal-contextual-lambda COMMAND te tslang_add_test(NAME test-compile-own-00-object-literal-contextual-lambda COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-00-interface-field-getter-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") tslang_add_test(NAME test-compile-00-conditional-record-literal-temporary COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") +tslang_add_test(NAME test-compile-00-interface-print COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_print.ts") tslang_add_test(NAME test-compile-00-class-method-devirtualized COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts") tslang_add_test(NAME test-compile-00-call-arg-param-field-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-compile-00-class-accessor-untyped-setter COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") @@ -1257,6 +1258,7 @@ tslang_add_test(NAME test-jit-00-object-literal-contextual-lambda COMMAND test-r tslang_add_test(NAME test-jit-own-00-object-literal-contextual-lambda COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-00-interface-field-getter-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") tslang_add_test(NAME test-jit-00-conditional-record-literal-temporary COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") +tslang_add_test(NAME test-jit-00-interface-print COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_print.ts") tslang_add_test(NAME test-jit-00-class-method-devirtualized COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts") tslang_add_test(NAME test-jit-00-call-arg-param-field-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-jit-00-class-accessor-untyped-setter COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") @@ -2274,6 +2276,7 @@ set(TSLANG_CORPUS 00object_literal_contextual_lambda.ts 00interface_field_getter_owned.ts 00conditional_record_literal_temporary.ts + 00interface_print.ts 00class_method_devirtualized.ts 00call_arg_param_field_global.ts 00class_accessor_untyped_setter.ts diff --git a/tslang/test/tester/tests/00interface_print.ts b/tslang/test/tester/tests/00interface_print.ts new file mode 100644 index 000000000..8af51209e --- /dev/null +++ b/tslang/test/tester/tests/00interface_print.ts @@ -0,0 +1,73 @@ +// @strict-null false +// An interface value printed, or put into a string: its `toString` when it has one, as a class's is; +// else its fields, read through it, as an object literal's are; null as null. It crashed at run time, +// whatever object was behind it. A field of its own type again prints `[Object]`. +interface Thing { + n: number; + name: string; +} + +interface Named { + label: string; + toString(): string; +} + +interface Base { + a: number; +} + +interface Derived extends Base { + b: string; +} + +interface Chain { + v: number; + next: Chain; +} + +class Ball implements Thing { + constructor(public n: number, public name: string) {} +} + +@value +interface Hit { + readonly thing: Thing; + readonly dist: number; +} + +function find(d: number): Hit | null { + if (d < 0) return null; + return { thing: new Ball(1, "ball"), dist: d }; +} + +function main() { + const t: Thing = new Ball(3, "b"); + assert(`${t}` == "{ n: 3, name: 'b' }", "a class seen through an interface"); + + const lit: Thing = { n: 4, name: "lit" }; + assert(`${lit}` == "{ n: 4, name: 'lit' }", "an object literal seen through an interface"); + + const none: Thing = null; + assert(`${none}` == "null"); + + const named: Named = { + label: "x", + toString() { + return "Named(" + this.label + ")"; + } + }; + assert(`${named}` == "Named(x)", "its toString"); + + const base: Base = { a: 1 }; + const derived: Derived = { a: base.a, b: "two" }; + assert(`${derived}` == "{ a: 1, b: 'two' }", "the fields of what it extends first"); + + const chain: Chain = { v: 1, next: { v: 2, next: null } }; + assert(`${chain}` == "{ v: 1, next: [Object] }"); + + assert(`${find(2)}` == "{ thing: { n: 1, name: 'ball' }, dist: 2 }", "a @value value holding an interface"); + assert(`${find(-1)}` == "null"); + + print(t, lit, named); + print("done."); +} From 2da1465dc4ca1fd759a23fbc95d10c2929b75f8d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 12:28:23 +0000 Subject: [PATCH 3/3] A C string result is copied when an imported module loaded its .d.ts first TSLANG_ISSUES 21. The string a C function returns is copied into a string of the program's own (#575), but not when the `.d.ts` declaring it was first reached through a module this file imports: `declaredFunctions` kept the module of that earlier pass, and isForeignCall wanted the one being generated. The raw `char*` was taken as it was - stored strerror() results showed the buffer's last text, and under -mm=rc the count was written into C's memory and the release freed it (native_ts_graphics on Android: "Scudo ERROR: misaligned pointer when deallocating"). A top-level body-less `declare function` is C whichever module's pass loaded its `.d.ts`: isForeignCall now asks that, and that this module's function has no body. Test c-string-import/ (gc, rc, none, own; compiled and JIT), whose `.d.ts` is outside tests/, where *.d.ts are build output and ignored. ctest 6560/6560; fuzz seeds 85000-85599 clean. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 13 ++++++++----- tslang/test/tester/CMakeLists.txt | 6 ++++++ .../c-string-import/c_string_copy_decls.d.ts | 2 ++ .../c-string-import/c_string_copy_import.ts | 18 ++++++++++++++++++ .../c-string-import/c_string_copy_lib.ts | 7 +++++++ 5 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/c-string-import/c_string_copy_decls.d.ts create mode 100644 tslang/test/tester/c-string-import/c_string_copy_import.ts create mode 100644 tslang/test/tester/c-string-import/c_string_copy_lib.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 3206dffa7..2ce4409cc 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -10180,9 +10180,12 @@ class MLIRGenImpl } auto name = symbolRef.getIdentifier().str(); - auto declared = declaredFunctions.find(name); - if (declared == declaredFunctions.end() || declared->second.second != theModule.getOperation() - || !rootDeclaredFunctions.contains(name)) + // Declared at the top level with no body, by whichever module's pass reached its .d.ts first: + // the entry keeps the module of that pass, which is not this one when an imported module + // referenced the same .d.ts (a `/// ` is loaded once), and the string a C function + // returned was then taken as it was, not copied (TSLANG_ISSUES 21). Whether it is C is + // whether this module's function has a body, below. + if (!declaredFunctions.count(name) || !rootDeclaredFunctions.contains(name)) { return false; } @@ -14921,8 +14924,8 @@ class MLIRGenImpl ValueOrLogicalResult castConstArrayToString(mlir::Location location, mlir::Value value, const GenContext &genContext); ValueOrLogicalResult castTupleToString(mlir::Location location, mlir::Value value, mlir_ts::TupleType tupleType, - ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, const GenContext &genContext); - + ::llvm::ArrayRef<::mlir::typescript::FieldInfo> fields, const GenContext &genContext); + ValueOrLogicalResult fieldsToString(mlir::Location location, ::llvm::ArrayRef> fieldValues, const GenContext &genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 812e73da4..3b5ddd0e7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1567,6 +1567,12 @@ tslang_add_test(NAME test-compile-include-global-var COMMAND test-runner "${PROJ # imports support only compile mode tslang_add_test(NAME test-compile-import-component COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/component.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/service.ts") +# A C function declared in a .d.ts that an imported module referenced first: its string result is still +# copied (TSLANG_ISSUES 21) +foreach(c_string_mm gc rc none own) + tslang_add_test(NAME test-compile-import-c-string-copy-${c_string_mm} COMMAND test-runner -mm=${c_string_mm} "${PROJECT_SOURCE_DIR}/test/tester/c-string-import/c_string_copy_import.ts" "${PROJECT_SOURCE_DIR}/test/tester/c-string-import/c_string_copy_lib.ts") + tslang_add_test(NAME test-jit-import-c-string-copy-${c_string_mm} COMMAND test-runner -jit -mm=${c_string_mm} "${PROJECT_SOURCE_DIR}/test/tester/c-string-import/c_string_copy_import.ts") +endforeach() # A library whose root only declares and initializes a variable must not get an entry point of # its own: it is compiled with the same --emit=obj as the program linked beside it, and a `main` # in both fails to link. Only the program half is given --entry-point. diff --git a/tslang/test/tester/c-string-import/c_string_copy_decls.d.ts b/tslang/test/tester/c-string-import/c_string_copy_decls.d.ts new file mode 100644 index 000000000..d1bd4332c --- /dev/null +++ b/tslang/test/tester/c-string-import/c_string_copy_decls.d.ts @@ -0,0 +1,2 @@ +// C functions for c_string_copy_import.ts and c_string_copy_lib.ts, which both reference this file +declare function strerror(errnum: s32): string; diff --git a/tslang/test/tester/c-string-import/c_string_copy_import.ts b/tslang/test/tester/c-string-import/c_string_copy_import.ts new file mode 100644 index 000000000..58e77bc5d --- /dev/null +++ b/tslang/test/tester/c-string-import/c_string_copy_import.ts @@ -0,0 +1,18 @@ +// regression test (TSLANG_ISSUES 21): the string a C function returns is copied into a string of the +// program's own (00c_string_copy.ts) also when the .d.ts declaring it was first loaded by a module this +// file imports. The declaration was recorded with that pass's module, and the copy was skipped: both +// stored `strerror` results showed the buffer's last text, and under -mm=rc the count was written into +// C's memory and the release freed it. +/// +import { Other } from "./c_string_copy_lib"; + +function main() { + const messages: string[] = []; + messages.push(strerror(2)); + messages.push(strerror(13)); + assert(messages[0] != messages[1], "each result is a copy of its own"); + assert(messages[0].length > 0 && messages[1].length > 0); + assert(new Other().name() == "other"); + print(messages[0], "|", messages[1]); + print("done."); +} diff --git a/tslang/test/tester/c-string-import/c_string_copy_lib.ts b/tslang/test/tester/c-string-import/c_string_copy_lib.ts new file mode 100644 index 000000000..7a3632adc --- /dev/null +++ b/tslang/test/tester/c-string-import/c_string_copy_lib.ts @@ -0,0 +1,7 @@ +// The module c_string_copy_import.ts imports. It references the same .d.ts, and is generated first. +/// +export class Other { + name(): string { + return "other"; + } +}