From 83659cd486cf519a1ee9928fed862ce91762987f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 10:55:24 +0000 Subject: [PATCH 1/2] A union holds null when its tag's descriptor is of kind null, not when its name is "null" A tagged union compared with null compared its tag's text with "null" (StringCompareOp, a strcmp) - every `Intersection | null` result of the ray tracer's Thing.intersect, and every traceRay and testRay. The tag points into its type's descriptor (TypeOfOpHelper::typeDescriptorValue), whose kind says null: one load and one compare. A tag never set reads undefined's descriptor, so it is not null, as before. Ray tracer, median ms: gc 79 -> 75, none 79 -> 74, rc 120 -> 115, own 86 -> 82. The hot path has no strcmp left; removing the union altogether (README variant B) now saves about 6 ms under none. ctest 6534/6534; fuzz seeds 81000-81599 clean. Co-Authored-By: Claude Opus 5.5 --- tslang/benchmarks/raytrace/README.md | 20 +++++++++++-------- .../LowerToLLVM/UnaryBinLogicalOrHelper.h | 20 ++++++++++++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/tslang/benchmarks/raytrace/README.md b/tslang/benchmarks/raytrace/README.md index 68a2a28b1..c569d00ce 100644 --- a/tslang/benchmarks/raytrace/README.md +++ b/tslang/benchmarks/raytrace/README.md @@ -26,7 +26,7 @@ pwsh tslang/benchmarks/raytrace/bench.ps1 - Rust: `opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"` - C++: `clang++ -std=c++20 -O3`, and `cl /std:c++20 /O2 /EHsc /MT` -## Results 2026-10-31: parameters captured by copy, direct method calls +## Results 2026-10-31: parameters captured by copy, direct method calls, null tested by kind Release, Windows 11 x64, the same machine; main 1cd17c33 against the same tree with each change added in turn. Median of 7 runs after a warm-up; every build prints the same checksum. Neither @@ -44,16 +44,20 @@ benchmark file changed. inline it. A class that code outside the module may extend (exported, declared or imported, a C++ binding, or a base of one of these) keeps its methods virtual. That takes in `RayTracer`'s methods: variant F below, now without the source edit. +- Null tested by kind: a union's tag points into its type's descriptor, so whether it holds null + is the descriptor's kind (`TYPE_KIND_NULL`), one load and one compare. It was a `strcmp` of the + type's name with `"null"`, on every `intersect` result and every `traceRay` and `testRay`. -| Build | main | parameters by copy | and direct method calls | vs Rust idiomatic (55 ms) | -|---|---|---|---|---| -| tslang gc | 266 ms | 97 ms | 79 ms | 1.4x | -| tslang rc | 171 ms | 133 ms | 120 ms | 2.2x | -| tslang none | 157 ms | 96 ms | 79 ms | 1.4x | -| tslang own | 143 ms | 104 ms | 86 ms | 1.6x | +| Build | main | parameters by copy | and direct method calls | and null by kind | vs Rust idiomatic (55 ms) | +|---|---|---|---|---|---| +| tslang gc | 266 ms | 97 ms | 79 ms | 75 ms | 1.4x | +| tslang rc | 171 ms | 133 ms | 120 ms | 115 ms | 2.1x | +| tslang none | 157 ms | 96 ms | 79 ms | 74 ms | 1.3x | +| tslang own | 143 ms | 104 ms | 86 ms | 82 ms | 1.5x | Of the three costs measured below, A's cells and F's virtual calls are now gone without editing -the source; B, `Intersection | null`, measured 16-17 ms under none and own, and remains. +the source, and B is mostly gone: variant B (`intersect` returns `dist: 0` for a miss, no union) +now runs in 69.5 ms under none against 75 ms. ## Results 2026-10-24: `@value` diff --git a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h index e1bfe8606..e4e4d2161 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h @@ -8,6 +8,8 @@ #include "TypeScript/TypeScriptOps.h" #include "TypeScript/LowerToLLVM/LLVMTypeConverterHelper.h" +#include "TypeScript/LowerToLLVM/TypeConverterHelper.h" +#include "TypeScript/LowerToLLVM/TypeDescriptorLogic.h" #include "TypeScript/MLIRLogic/TypeOfOpHelper.h" #include "TypeScript/LowerToLLVM/LLVMCodeHelperBase.h" @@ -129,11 +131,23 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ return builder.create(loc, v1, pointerWord, zero); } + // The tag points into the type's descriptor (TypeOfOpHelper::typeDescriptorValue), whose kind + // says null in one load and one compare - not a strcmp of the name with "null", which every + // `Intersection | null` result paid for. A tag never set (a zeroed union) is not null, as it + // was not equal to "null": it reads the descriptor of undefined instead. auto tagValue = builder.create(loc, llvmtch.typeConverter->convertType(mth.getStringType()), llvmUnion, MLIRHelper::getStructIndex(builder, UNION_TAG_INDEX)); - auto nullStr = builder.create(loc, mth.getStringType(), builder.getStringAttr("null")); - return builder.create( - loc, mth.getBooleanType(), tagValue, nullStr, builder.getI32IntegerAttr((int)op)); + auto tagIsSet = builder.create(loc, LLVM::ICmpPredicate::ne, tagValue, + builder.create(loc, tagValue.getType())); + TypeOfOpHelper toh(builder); + auto undefinedTag = builder.create( + loc, tagValue.getType(), toh.typeDescriptorValue(loc, mlir_ts::UndefinedType::get(builder.getContext()))); + auto readTag = builder.create(loc, tagIsSet, tagValue, undefinedTag); + TypeConverterHelper tch(&typeConverter); + TypeDescriptorLogic tdl(builder, tch, loc); + auto isNull = tdl.isKind(readTag, TYPE_KIND_NULL); + auto trueValue = builder.create(loc, isNull.getType(), builder.getBoolAttr(true)); + return builder.create(loc, v1, isNull, trueValue); }; if (isa(leftType) || isa(rightType)) From 63be2942711b3565c051467f169bf4fa5a9bcf19 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 11:18:48 +0000 Subject: [PATCH 2/2] A @value type holding an interface or a class needs no tag for its null `T | null` of a @value type was a tagged union inside an optional, 88 bytes for the ray tracer's Intersection. An interface is null when the object in it is (`{vtable, this}`, `this` null), and a class when its pointer is, so a T holding either holds its own null: MLIRTypeHelper::getValueNullNiche finds the first such field (looking into @value fields too), and isUnionTypeNeedsTag answers no, base type T. - null into it is T zeroed (CastLogicHelper); `== null` reads that pointer (UnaryBinLogicalOrHelper valueNullCompare); `typeof` and boxing into `any` ask the same (TypeOfOpHelper, castToAny). Undefined is still the empty optional, so null and undefined stay apart. - -mm=own: a @value value widened to its own `T | null`, or read back, is a view (isView) - the cast stays a cast where a tagged one became CreateUnionInstanceOp. - A T with only numbers keeps its tag. Under `// @strict-null false` a T built with that field null now reads as null (spec "Missing values"). Ray tracer, median ms: gc 76 -> 72, none 76 -> 70, rc 117 -> 111, own 83 -> 77; the same as the variant with no union at all. Test value/value_missing_niche (gc, rc, none). ctest 6540/6540; fuzz seeds 82000-82599 clean; DefaultLib compiles under all four models. Co-Authored-By: Claude Opus 5.5 --- tslang/benchmarks/raytrace/README.md | 29 ++- .../specs/2026-10-24-value-types-design.md | 8 + .../TypeScript/LowerToLLVM/CastLogicHelper.h | 15 ++ .../LowerToLLVM/UnaryBinLogicalOrHelper.h | 35 +++ .../TypeScript/MLIRLogic/MLIRTypeHelper.h | 50 +++++ .../TypeScript/MLIRLogic/TypeOfOpHelper.h | 21 ++ tslang/lib/TypeScript/OwnershipFacts.h | 12 +- tslang/test/tester/CMakeLists.txt | 7 + .../test/tester/value/value_missing_niche.ts | 205 ++++++++++++++++++ 9 files changed, 369 insertions(+), 13 deletions(-) create mode 100644 tslang/test/tester/value/value_missing_niche.ts diff --git a/tslang/benchmarks/raytrace/README.md b/tslang/benchmarks/raytrace/README.md index c569d00ce..a52c9f9d3 100644 --- a/tslang/benchmarks/raytrace/README.md +++ b/tslang/benchmarks/raytrace/README.md @@ -26,7 +26,7 @@ pwsh tslang/benchmarks/raytrace/bench.ps1 - Rust: `opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"` - C++: `clang++ -std=c++20 -O3`, and `cl /std:c++20 /O2 /EHsc /MT` -## Results 2026-10-31: parameters captured by copy, direct method calls, null tested by kind +## Results 2026-10-31: parameters captured by copy, direct method calls, null without a tag Release, Windows 11 x64, the same machine; main 1cd17c33 against the same tree with each change added in turn. Median of 7 runs after a warm-up; every build prints the same checksum. Neither @@ -47,17 +47,22 @@ benchmark file changed. - Null tested by kind: a union's tag points into its type's descriptor, so whether it holds null is the descriptor's kind (`TYPE_KIND_NULL`), one load and one compare. It was a `strcmp` of the type's name with `"null"`, on every `intersect` result and every `traceRay` and `testRay`. - -| Build | main | parameters by copy | and direct method calls | and null by kind | vs Rust idiomatic (55 ms) | -|---|---|---|---|---|---| -| tslang gc | 266 ms | 97 ms | 79 ms | 75 ms | 1.4x | -| tslang rc | 171 ms | 133 ms | 120 ms | 115 ms | 2.1x | -| tslang none | 157 ms | 96 ms | 79 ms | 74 ms | 1.3x | -| tslang own | 143 ms | 104 ms | 86 ms | 82 ms | 1.5x | - -Of the three costs measured below, A's cells and F's virtual calls are now gone without editing -the source, and B is mostly gone: variant B (`intersect` returns `dist: 0` for a miss, no union) -now runs in 69.5 ms under none against 75 ms. +- Null in the reference: `Intersection` holds an interface, `thing`, and an interface is null when + the object in it is (`{vtable, this}`, `this` null). So `Intersection | null` needs no tag: it is + stored as an `Intersection`, null being one whose `thing` holds no object, and the 88-byte + tagged result of `intersect` is the value itself with the optional's flag. A `@value` type with + only numbers keeps its tag. + +| Build | main | parameters by copy | and direct method calls | and null by kind | and null in the reference | vs Rust idiomatic (55 ms) | +|---|---|---|---|---|---|---| +| tslang gc | 266 ms | 97 ms | 79 ms | 75 ms | 71 ms | 1.3x | +| tslang rc | 171 ms | 133 ms | 120 ms | 115 ms | 110 ms | 2.0x | +| tslang none | 157 ms | 96 ms | 79 ms | 74 ms | 70 ms | 1.3x | +| tslang own | 143 ms | 104 ms | 86 ms | 82 ms | 77 ms | 1.4x | + +Of the three costs measured below, A's cells, F's virtual calls and B's union are now gone without +editing the source: variant B (`intersect` returns `dist: 0` for a miss, no union) runs in 69.9 ms +under none, as the benchmark itself now does. ## Results 2026-10-24: `@value` diff --git a/tslang/docs/superpowers/specs/2026-10-24-value-types-design.md b/tslang/docs/superpowers/specs/2026-10-24-value-types-design.md index 18c0a8146..f71f5a26c 100644 --- a/tslang/docs/superpowers/specs/2026-10-24-value-types-design.md +++ b/tslang/docs/superpowers/specs/2026-10-24-value-types-design.md @@ -195,6 +195,14 @@ TypeScript lets `null` and `undefined` stand for an `Intersection`. A value has `object` tag and the tuple. `=== undefined` reads the `i1`; `=== null` and `== null` read it and then compare the tag with the string `"null"` (`strcmp` when the tag is not a null pointer). This is the cost the benchmark README measures for `Intersection | null`. +- 2026-10-31: the `null` test reads the tag's descriptor kind (`TYPE_KIND_NULL`) instead of the + `strcmp`. And a `T` that holds an interface or a class (`Intersection.thing`), directly or in a + `@value` field, holds null in it: an interface is null when its `this` is, a class when its + pointer is (`MLIRTypeHelper::getValueNullNiche`, the first such field). Its union has no tag + (`isUnionTypeNeedsTag` answers no, base type `T`): null is `T` zeroed, `== null` reads that + pointer, and `T | null` is passed as `{ T, i1 }`. Undefined is still the empty optional, so null + and undefined stay apart. Under `// @strict-null false` a `T` built with that field null + (`{ thing: null, ... }`) therefore reads as null. - Strict comparisons with `null` and `undefined` are exact, as in TypeScript: `x === undefined` is false for `null`, and `x === null` false for `undefined`. Loose `x == null` and `x != null` ask whether `x` holds a value. Comparing one with anything else (another value, a number) is the diff --git a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h index ef7eef20d..fd329d274 100644 --- a/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h @@ -739,6 +739,13 @@ class CastLogicHelper } else { + // null into `T | null` of a @value type that holds null in a pointer of its own + // (MLIRTypeHelper::getValueNullNiche): T with every field zeroed, that pointer too + if (isa(inType) && isa(baseType)) + { + return rewriter.create(loc, tch.convertType(baseType)); + } + return cast(in, inType, tch.convertType(inType), baseType, tch.convertType(baseType)); } } @@ -1302,6 +1309,14 @@ class CastLogicHelper auto nullTag = toh.typeOfLogic(loc, mlir_ts::NullType::get(rewriter.getContext())); mlir::Value pointer = in.getType() == inLLVMType ? in : rewriter.create(loc, inLLVMType, in); + // ...or a @value type that holds null in a pointer of its own (MLIRTypeHelper:: + // getValueNullNiche): that pointer + if (auto valueType = dyn_cast(baseType)) + { + auto niche = MLIRTypeHelper::getValueNullNiche(valueType); + pointer = rewriter.create(loc, pointer, llvm::SmallVector(niche.begin(), niche.end())); + } + auto isSet = rewriter.create(loc, LLVM::ICmpPredicate::ne, pointer, rewriter.create(loc, th.getPtrType())); auto tagType = setTag.getType(); diff --git a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h index e4e4d2161..c15f625b8 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h @@ -150,6 +150,28 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ return builder.create(loc, v1, isNull, trueValue); }; + // `T | null` of a @value type T that holds null in a pointer of its own (MLIRTypeHelper:: + // getValueNullNiche) is stored as T: it is null when that pointer is + auto valueNullCompare = [&](mlir::Value unionValue, mlir_ts::UnionType unionType) -> mlir::Value { + auto valueType = ::typescript::MLIRTypeHelper::getMissingValueElement(unionType); + if (!valueType) + { + return mlir::Value(); + } + + auto niche = ::typescript::MLIRTypeHelper::getValueNullNiche(valueType); + if (niche.empty()) + { + return mlir::Value(); + } + + auto llvmValue = asLLVMUnion(unionValue, unionType); + llvm::SmallVector path(niche.begin(), niche.end()); + auto pointerWord = builder.create(loc, llvmValue, path); + auto zero = builder.create(loc, pointerWord.getType()); + return builder.create(loc, v1, pointerWord, zero).getResult(); + }; + if (isa(leftType) || isa(rightType)) { return OptionalTypeLogicalOp(binOp, op, builder, typeConverter, compileOptions); @@ -262,6 +284,11 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ return nullCompare(right, unionType); } + if (auto compared = valueNullCompare(right, unionType)) + { + return compared; + } + // the union is the right operand, so it is the right type that becomes its base type: // passing the base type as the left type and the union again as the right one came // straight back here, until the compiler's stack ran out (`opaque != (opaque | null)`) @@ -356,6 +383,14 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ return mlir::Value(); } + if (right.getDefiningOp() || right.getDefiningOp() || matchPattern(right, m_Zero())) + { + if (auto compared = valueNullCompare(left, unionType)) + { + return compared; + } + } + return LogicOp(binOp, op, left, baseType, right, rightType, builder, typeConverter, compileOptions); } else if (auto leftTupleType = dyn_cast(leftType)) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index 41fa414ec..6b191a21c 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -254,6 +254,48 @@ class MLIRTypeHelper return mlir_ts::OptionalType::get(mlir_ts::UnionType::get(valueType.getContext(), members)); } + // Where null of a @value type T is in T itself: the index path, into T's storage, of a pointer no + // value of T holds as null - the `this` of a field of an interface type (`{vtable, this}`, null + // exactly when the interface is), or a field of a class type - so `T | null` needs no tag: null is + // T with that pointer null (zeroed). Fields of a @value type T holds are looked into as well. + // Empty when T has no such field (only numbers, say), and its null keeps a tag of its own. + static llvm::SmallVector getValueNullNiche(mlir_ts::ValueType valueType) + { + llvm::SmallVector path; + auto storageType = dyn_cast(valueType.getStorageType()); + if (!storageType) + { + return path; + } + + for (auto [index, field] : llvm::enumerate(storageType.getFields())) + { + auto fieldType = field.type; + if (isa(fieldType)) + { + return {static_cast(index), THIS_VALUE_INDEX}; + } + + if (isa(fieldType)) + { + return {static_cast(index)}; + } + + if (auto innerValueType = dyn_cast(fieldType)) + { + auto inner = getValueNullNiche(innerValueType); + if (!inner.empty()) + { + path.push_back(static_cast(index)); + path.append(inner.begin(), inner.end()); + return path; + } + } + } + + return path; + } + // `T | null` of a @value type T, which getMissingValueType wraps in the optional static bool isMissingValueUnion(mlir_ts::UnionType unionType) { @@ -2447,6 +2489,14 @@ class MLIRTypeHelper // merged again would be the optional around it, not it if (isMissingValueUnion(unionType)) { + // ...unless T holds null in a pointer of its own (getValueNullNiche): then it is T + auto valueType = getMissingValueElement(unionType); + if (valueType && !getValueNullNiche(valueType).empty()) + { + baseType = valueType; + return false; + } + baseType = unionType; return true; } diff --git a/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h b/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h index e7735f479..47264dbd2 100644 --- a/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/TypeOfOpHelper.h @@ -376,6 +376,27 @@ class TypeOfOpHelper return rewriter.create(loc, mlir_ts::StringType::get(rewriter.getContext()), value); } + // `T | null` of a @value type that holds null in a pointer of its own (MLIRTypeHelper:: + // getValueNullNiche) is T with no tag: null is that pointer null, so asked here + if (isa(baseType)) + { + auto stringType = mlir_ts::StringType::get(rewriter.getContext()); + auto nullValue = rewriter.create(loc, mlir_ts::NullType::get(rewriter.getContext())); + auto isNull = rewriter.create( + loc, mlir_ts::BooleanType::get(rewriter.getContext()), + rewriter.getI32IntegerAttr((int)SyntaxKind::EqualsEqualsEqualsToken), value, nullValue); + auto ifOp = rewriter.create(loc, stringType, isNull, true); + + rewriter.setInsertionPointToStart(&ifOp.getThenRegion().back()); + rewriter.create(loc, typeDescriptorValue(loc, mlir_ts::NullType::get(rewriter.getContext()))); + + rewriter.setInsertionPointToStart(&ifOp.getElseRegion().back()); + rewriter.create(loc, typeOfLogic(loc, baseType)); + + rewriter.setInsertionPointAfter(ifOp); + return ifOp.getResult(0); + } + origType = baseType; } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 81d1dedf9..581c8677c 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -393,8 +393,18 @@ inline bool isView(mlir::Operation *op) return valueType && valueType.getStorageType() == tuple; }; + // ...or a `@value` value seen as its own `T | null`, or read back from it: when T holds null in a + // pointer of its own (MLIRTypeHelper::getValueNullNiche) the union is T itself, no tag added, and + // the cast stays a cast where a tagged one becomes CreateUnionInstanceOp + auto isMissingValueView = [](mlir::Type value, mlir::Type missing) { + auto valueType = mlir::dyn_cast(value); + return valueType && mlir::isa(missing) && + MLIRTypeHelper::getMissingValueElement(missing) == valueType; + }; + return (keeps(in) && keeps(out)) || (isClosure(in) && isClosure(out)) || isMadeObject() || isNullableView || - isValueStorage(in, out) || isValueStorage(out, in); + isValueStorage(in, out) || isValueStorage(out, in) || isMissingValueView(in, out) || + isMissingValueView(out, in); } // The block a value is a view of. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e3e8f0ebe..7cdae37df 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3385,6 +3385,13 @@ foreach(value_mm gc rc none own) tslang_add_test(NAME test-compile-value-missing-${value_mm} COMMAND test-runner -mm=${value_mm} "${PROJECT_SOURCE_DIR}/test/tester/value/value_missing.ts") tslang_add_test(NAME test-jit-value-missing-${value_mm} COMMAND test-runner -jit -mm=${value_mm} "${PROJECT_SOURCE_DIR}/test/tester/value/value_missing.ts") endforeach() +# ...the same of a @value type holding an interface or a class, whose `T | null` has no tag: null is +# that reference null (MLIRTypeHelper::getValueNullNiche). Not under own, which refuses the program's +# records holding a borrowed global (`thing: ball`) in fields and loops; own_raytrace covers own. +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() # ...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_niche.ts b/tslang/test/tester/value/value_missing_niche.ts new file mode 100644 index 000000000..f7cee35a9 --- /dev/null +++ b/tslang/test/tester/value/value_missing_niche.ts @@ -0,0 +1,205 @@ +// @strict-null false +// value_missing.ts with a reference in the @value type: `Hit.thing` is an interface, so a Hit is +// null when the object in its `thing` is null, and `Hit | null` needs no tag (the `Mark` below holds a +// class instead). As there, `T | null`, `T | undefined`, `T | null | undefined`, `v?: T` and `f?: T` +// are one optional value that keeps which of null and undefined it holds, as TypeScript does. It is returned, passed, held in a field, converted between those spellings, compared with +// null and undefined, switched on, and read after a check or through `?.`. +interface Thing { + n: number; +} + +class Ball implements Thing { + constructor(public n: number) {} +} + +const ball = new Ball(1); + +@value +interface Hit { + readonly thing: Thing; + readonly dist: number; +} + +class Owner { + constructor(public id: number) {} +} + +@value +interface Mark { + readonly at: number; + readonly owner: Owner; +} + +function mark(d: number): Mark | null { + if (d < 0) return null; + return { at: d, owner: new Owner(d) }; +} + +interface Holder { + hit?: Hit; +} + +class Box { + hit?: Hit; +} + +class HitBox implements Holder { + hit?: Hit; +} + +interface Based { + x: number; +} + +// `f` is a member of one side only: of the intersection, an optional one +type Either = Based & ({ f: Hit } | { g: number }); + +function find(d: number): Hit | null { + if (d < 0) return null; + return { thing: ball, dist: d }; +} + +function first(xs: number[]): Hit | undefined { + let best: Hit | undefined = undefined; + for (const x of xs) { + const h = find(x); + if (h != null && (best === undefined || h.dist < best.dist)) best = h; + } + + return best; +} + +function opt(v?: Hit) { + return v === undefined ? -1 : v.dist; +} + +// what an optional parameter was given: nothing, null or a value +function state(v?: Hit) { + return v === undefined ? 0 : v === null ? 1 : 2; +} + +function sw(d: number) { + switch (find(d)) { + case null: + return 1; + default: + return 2; + } +} + +function swu(xs: number[]) { + switch (first(xs)) { + case undefined: + return 1; + default: + return 2; + } +} + +function main() { + assert(find(-1) === null); + assert(!(find(-1) != null)); + assert(find(2).dist == 2); + + assert(first([3, -1, 2]).dist == 2); + assert(first([-1]) === undefined); + + assert(find(5)?.dist == 5); + assert(find(-5)?.dist === undefined); + + assert(opt() == -1); + assert(opt({ thing: ball, dist: 4 }) == 4); + + const none: Holder = {}; + const some: Holder = { hit: { thing: ball, dist: 8 } }; + assert(none.hit === undefined && none.hit?.dist === undefined); + assert(some.hit !== undefined && some.hit.dist == 8); + + const box = new Box(); + assert(box.hit === undefined); + box.hit = find(7); + assert(box.hit.dist == 7); + + // a class's `hit?: Hit` seen through the interface, in each state, and written through it + const hitBox = new HitBox(); + const viaImplements: Holder = hitBox; + assert(viaImplements.hit === undefined && !(viaImplements.hit === null)); + hitBox.hit = null; + assert(viaImplements.hit === null && !(viaImplements.hit === undefined)); + hitBox.hit = find(6); + assert(viaImplements.hit.dist == 6); + viaImplements.hit = find(2); + assert(hitBox.hit.dist == 2); + viaImplements.hit = null; + assert(hitBox.hit === null && !(hitBox.hit === undefined)); + viaImplements.hit = undefined; + assert(hitBox.hit === undefined && !(hitBox.hit === null)); + + // ...and of one that does not say it implements it + const plainBox = new Box(); + const viaShape: Holder = plainBox; + assert(viaShape.hit === undefined && !(viaShape.hit === null)); + plainBox.hit = null; + assert(viaShape.hit === null && !(viaShape.hit === undefined)); + plainBox.hit = find(4); + assert(viaShape.hit.dist == 4); + viaShape.hit = find(3); + assert(plainBox.hit.dist == 3); + viaShape.hit = null; + assert(plainBox.hit === null); + viaShape.hit = undefined; + assert(plainBox.hit === undefined); + + const withF: Either = { x: 1, f: { thing: ball, dist: 9 } }; + assert(withF.f.dist == 9); + const withG: Either = { x: 2, g: 3 }; + assert(withG.f === undefined && !(withG.f === null)); + + let u: Hit | null | undefined = null; + assert(!(u === undefined)); + assert(u === null); + + // null and undefined stay apart, whichever spelling holds them + assert(!(find(-1) === undefined)); + assert(!(first([-1]) === null)); + let n: Hit | null | undefined = find(-1); + assert(n === null && !(n === undefined)); + n = first([-1]); + assert(n === undefined && !(n === null)); + n = find(9); + assert(n !== null && n !== undefined && n.dist == 9); + const asUndefinable: Hit | undefined = find(-1); + assert(asUndefinable === null); + const asNullable: Hit | null = first([-1]); + assert(asNullable === undefined); + const present: Hit | null = first([5]); + assert(present.dist == 5); + assert(find(-1) == null && first([-1]) == null && find(-1) == undefined); + + assert(state() == 0); + assert(state(null) == 1); + assert(state({ thing: ball, dist: 1 }) == 2); + + assert(sw(-1) == 1); + assert(sw(3) == 2); + assert(swu([-1]) == 1); + assert(swu([3]) == 2); + + // the reference in a value read back, and a class reference standing for null + assert(find(3).thing.n == 1); + assert(mark(-1) === null && mark(-1) == null && !(mark(-1) === undefined)); + assert(mark(4).owner.id == 4 && mark(4).at == 4); + const marks: (Mark | null)[] = [mark(1), mark(-1), mark(2)]; + let seen = 0; + for (const m of marks) if (m != null) seen += m.owner.id; + assert(seen == 3); + + // typeof, and boxed: null as null, a value as an object + assert(typeof find(-1) == "null" && typeof find(2) == "object"); + const boxedNull: any = find(-1); + const boxedHit: any = find(2); + assert(boxedNull === null && typeof boxedNull == "null"); + assert(!(boxedHit === null) && typeof boxedHit == "object"); + + print("done."); +}