diff --git a/tslang/include/TypeScript/LowerToLLVM/OptionalLogicHelper.h b/tslang/include/TypeScript/LowerToLLVM/OptionalLogicHelper.h index 57daa09e9..6df44a906 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OptionalLogicHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/OptionalLogicHelper.h @@ -38,8 +38,6 @@ class OptionalLogicHelper template mlir::Value logicalOp(SyntaxKind opCmpCode) { - auto loc = binOp->getLoc(); - auto left = binOp->getOperand(0); auto right = binOp->getOperand(1); auto leftType = left.getType(); @@ -67,21 +65,68 @@ class OptionalLogicHelper return whenOneValueIsUndef(opCmpCode, right, left); } - // TODO: rewrite code to take in account that Opt value can be undefined - if (leftOptType) - { - auto leftSubType = leftOptType.getElementType(); - left = rewriter.create(loc, leftSubType, left); - } + return whenOneOptValue(opCmpCode); + } - if (rightOptType) - { - auto rightSubType = rightOptType.getElementType(); - right = rewriter.create(loc, rightSubType, right); - } + // One side is optional, the other a value. An optional holding undefined equals no value - not + // 0, not false, not null - so the values are compared only when it holds one: unwrapped without + // that test it read whatever the empty optional stored, and `undefined === 0` was true. + template + mlir::Value whenOneOptValue(SyntaxKind opCmpCode) + { + auto loc = binOp->getLoc(); + + TypeHelper th(rewriter); + CodeLogicHelper clh(binOp, rewriter); + + auto llvmBoolType = typeConverter.convertType(th.getBooleanType()); - return LogicOp(binOp, opCmpCode, left, left.getType(), right, right.getType(), rewriter, - typeConverter, compileOptions); + auto left = binOp->getOperand(0); + auto right = binOp->getOperand(1); + auto leftOptType = dyn_cast(left.getType()); + auto rightOptType = dyn_cast(right.getType()); + auto otherIsNull = isa((leftOptType ? right : left).getType()); + + auto hasValueBool = rewriter.create(loc, th.getBooleanType(), leftOptType ? left : right); + auto hasValue = rewriter.create(loc, llvmBoolType, hasValueBool); + + return clh.conditionalExpressionLowering( + loc, llvmBoolType, hasValue, + [&](OpBuilder &builder, Location loc) { + if (leftOptType) + { + left = rewriter.create(loc, leftOptType.getElementType(), left); + } + + if (rightOptType) + { + right = rewriter.create(loc, rightOptType.getElementType(), right); + } + + mlir::Value result = LogicOp( + binOp, opCmpCode, left, left.getType(), right, right.getType(), rewriter, typeConverter, compileOptions); + if (result && result.getType() != llvmBoolType) + { + result = rewriter.create(loc, llvmBoolType, result); + } + + return result; + }, + [&](OpBuilder &builder, Location loc) { + // undefined against a value: only "not equal" holds, and no ordering does - except + // that loosely undefined equals null + switch (opCmpCode) + { + case SyntaxKind::EqualsEqualsToken: + return clh.createI1ConstantOf(otherIsNull); + case SyntaxKind::ExclamationEqualsToken: + return clh.createI1ConstantOf(!otherIsNull); + case SyntaxKind::ExclamationEqualsEqualsToken: + return clh.createI1ConstantOf(true); + default: + return clh.createI1ConstantOf(false); + } + }); } template diff --git a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h index 057654f66..4d0b119b3 100644 --- a/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/UnaryBinLogicalOrHelper.h @@ -101,6 +101,15 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ LLVMTypeConverterHelper llvmtch(&typeConverter); + // A union's tag is read out of its LLVM struct. The lowering pattern hands over its operands + // converted already, but an optional's unwrapped value (OptionalLogicHelper) is still a ts.union. + auto asLLVMUnion = [&](mlir::Value value, mlir_ts::UnionType unionType) -> mlir::Value { + auto llvmUnionType = typeConverter.convertType(unionType); + return value.getType() == llvmUnionType + ? value + : builder.create(loc, llvmUnionType, value).getResult(); + }; + if (isa(leftType) || isa(rightType)) { return OptionalTypeLogicalOp(binOp, op, builder, typeConverter, compileOptions); @@ -212,8 +221,8 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ mlir::Type baseType; if (mth.isUnionTypeNeedsTag(loc, unionType, baseType)) { - auto tagValue = builder.create(loc, llvmtch.typeConverter->convertType(mth.getStringType()), right, - MLIRHelper::getStructIndex(builder, UNION_TAG_INDEX)); + auto tagValue = builder.create(loc, llvmtch.typeConverter->convertType(mth.getStringType()), + asLLVMUnion(right, unionType), MLIRHelper::getStructIndex(builder, UNION_TAG_INDEX)); auto nullStr = builder.create(loc, mth.getStringType(), builder.getStringAttr("null")); auto cmpOp = builder.create( loc, mth.getBooleanType(), tagValue, nullStr, builder.getI32IntegerAttr((int)op)); @@ -302,8 +311,8 @@ mlir::Value LogicOp(Operation *binOp, SyntaxKind op, mlir::Value left, mlir::Typ right.getDefiningOp() || right.getDefiningOp() || matchPattern(right, m_Zero()); if (isRightNullValue) { - auto tagValue = builder.create(loc, llvmtch.typeConverter->convertType(mth.getStringType()), left, - MLIRHelper::getStructIndex(builder, UNION_TAG_INDEX)); + auto tagValue = builder.create(loc, llvmtch.typeConverter->convertType(mth.getStringType()), + asLLVMUnion(left, unionType), MLIRHelper::getStructIndex(builder, UNION_TAG_INDEX)); auto nullStr = builder.create(loc, mth.getStringType(), builder.getStringAttr("null")); auto cmpOp = builder.create( loc, mth.getBooleanType(), tagValue, nullStr, builder.getI32IntegerAttr((int)op)); diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h index a06688a7f..d88f0a948 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRTypeHelper.h @@ -41,6 +41,13 @@ class MLIRTypeHelper { } + // the options are a copy, taken before a file's `// @strict-null` pragma is read: the pragma + // has to reach this copy too, or unions are built strict in a file compiled non-strict + void setStrictNullChecks(bool strictNullChecks) + { + compileOptions.strictNullChecks = strictNullChecks; + } + MLIRTypeHelper( mlir::MLIRContext *context, CompileOptions compileOptions, @@ -3166,6 +3173,14 @@ class MLIRTypeHelper return getUnionTypeWithMerge(location, types, mergeLiterals, mergeTypes); } + // Without strict null checks a nullable type (string, a class) holds null in its own pointer, so + // null goes away beside it. A union of several types is a tagged value, not one pointer: there + // null has nowhere to go but a member of its own. + bool needsNullMember(mlir::ArrayRef types) + { + return llvm::count_if(types, [](mlir::Type type) { return !isa(type); }) > 1; + } + // TODO: review all union merge logic mlir::Type getUnionTypeMergeTypes(mlir::Location location, UnionTypeProcessContext &unionContext, bool mergeLiterals = true, bool mergeTypes = true, bool disableStrickNullCheck = false) { @@ -3213,17 +3228,22 @@ class MLIRTypeHelper if (typesAll.size() == 1) { auto resType = typesAll.front(); - if (unionContext.isUndefined) + + // the null the checker needs goes in before undefined wraps the type: undefined makes it + // optional, it must not take the null away - `string | null | undefined` accepts null as + // `string | null` does + if (compileOptions.strictNullChecks && !disableStrickNullCheck && unionContext.isNullable + && !isa(resType)) { - return mlir_ts::OptionalType::get(resType); - } + resType = mlir_ts::UnionType::get(context, {resType, getNullType()}); + } - if (compileOptions.strictNullChecks && !disableStrickNullCheck && unionContext.isNullable) + if (unionContext.isUndefined) { - return mlir_ts::UnionType::get(context, {resType, getNullType()}); + return mlir_ts::OptionalType::get(resType); } - return resType; + return resType; } // merge types @@ -3233,7 +3253,7 @@ class MLIRTypeHelper mlir::SmallVector mergedTypesAll; this->mergeTypes(location, typesAll, mergedTypesAll); - if (compileOptions.strictNullChecks && unionContext.isNullable) + if (unionContext.isNullable && (compileOptions.strictNullChecks || needsNullMember(mergedTypesAll))) { mergedTypesAll.push_back(getNullType()); } @@ -3247,7 +3267,8 @@ class MLIRTypeHelper return retType; } - if (compileOptions.strictNullChecks && !disableStrickNullCheck && unionContext.isNullable) + if (unionContext.isNullable + && ((compileOptions.strictNullChecks && !disableStrickNullCheck) || needsNullMember(typesAll))) { typesAll.push_back(getNullType()); } diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 58df7e216..b44374677 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -679,6 +679,11 @@ namespace mlirgen return *result; } + if (auto result = castToNullableUnionType(location, type, value, valueType, genContext)) + { + return *result; + } + // const dest: cast via the unwrapped source type instead if (auto constType = dyn_cast(type)) { @@ -1342,6 +1347,40 @@ namespace mlirgen return std::nullopt; } + // `T | null` without a tag is T's own pointer, and null only there for the checker (strict null + // checks). A value of another type becomes a T first - an object literal an interface, a const + // array an array - the way it would be cast to T alone: cast straight to the union, that + // conversion was left undone for the lowering, which can't do it. + std::optional MLIRGenImpl::castToNullableUnionType(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext) + { + auto unionType = dyn_cast(type); + if (!unionType) + { + return std::nullopt; + } + + mlir::Type baseType; + if (mth.isUnionTypeNeedsTag(location, unionType, baseType) || !baseType || baseType == valueType) + { + return std::nullopt; + } + + if (isa(valueType) || isa(valueType) || isa(valueType) + || isa(valueType) || isa(valueType)) + { + return std::nullopt; + } + + auto types = unionType.getTypes(); + if (std::find(types.begin(), types.end(), valueType) != types.end()) + { + return std::nullopt; + } + + CAST(value, location, baseType, value, genContext); + return V(builder.create(location, type, value)); + } + std::optional MLIRGenImpl::castFromSourceSpecialCases(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext) { // union type to diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 8d45bd2af..207b34e28 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5861,6 +5861,54 @@ class MLIRGenImpl return mlir::success(); } + // An optional number compared with a number. The lowering compares them only when the optional + // holds a value (OptionalLogicHelper), so it stays optional here: synced as a plain value it was + // cast out of the optional first, an empty one reading as 0 - and the promotion saw only the other + // operand's type, so against an integer literal 0.5 became an s32 and `0.5 === 0` was true. Both + // sides take the wider of the optional's element type and the other operand's type. + ValueOrLogicalResult syncOptionalNumericTypes(mlir::Location location, mlir::Value &leftExpressionValue, mlir::Value &rightExpressionValue, const GenContext &genContext) + { + auto leftOptType = dyn_cast(leftExpressionValue.getType()); + auto rightOptType = dyn_cast(rightExpressionValue.getType()); + if (!leftOptType == !rightOptType) + { + return mlir::success(); + } + + auto &optValue = leftOptType ? leftExpressionValue : rightExpressionValue; + auto &otherValue = leftOptType ? rightExpressionValue : leftExpressionValue; + auto elementType = (leftOptType ? leftOptType : rightOptType).getElementType(); + auto otherType = otherValue.getType(); + if (auto literalType = dyn_cast(otherType)) + { + otherType = literalType.getElementType(); + } + + if (!isPromotableNumeric(elementType) || !isPromotableNumeric(otherType)) + { + return mlir::success(); + } + + auto types = numericPromotionOrder(); + auto commonType = llvm::find_if(types, [&](mlir::Type type) { return type == elementType || type == otherType; }); + if (commonType == types.end()) + { + return mlir::success(); + } + + if (elementType != *commonType) + { + CAST(optValue, location, getOptionalType(*commonType), optValue, genContext); + } + + if (otherValue.getType() != *commonType) + { + CAST(otherValue, location, *commonType, otherValue, genContext); + } + + return optValue; + } + // The order the arithmetic operators promote in, widest first: the first type either // operand already has is the one both are cast to. Shared by `+` and by the general // arithmetic/comparison path below, which is the point -- they disagreed, and `+` was @@ -6169,6 +6217,21 @@ class MLIRGenImpl // TODO: do we need to sync type for all Ops? auto types = numericPromotionOrder(); + // `-` and `*` fall into this case too: arithmetic on an optional is left as it was + if (opCode != SyntaxKind::MinusToken && opCode != SyntaxKind::AsteriskToken) + { + auto o = syncOptionalNumericTypes(location, leftExpressionValue, rightExpressionValue, genContext); + if (o.value) + { + break; + } + + if (mlir::failed(o.result)) + { + return mlir::failure(); + } + } + auto r = syncUnionTypes(location, leftExpressionValue, rightExpressionValue, genContext); if (r.value) { @@ -11335,6 +11398,7 @@ class MLIRGenImpl std::optional castToOptionalType(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); std::optional castToTaggedUnionType(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); + std::optional castToNullableUnionType(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); // union or optional or any or opaque source type std::optional castFromSourceSpecialCases(mlir::Location location, mlir::Type type, mlir::Value value, mlir::Type valueType, const GenContext &genContext); diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index 6347a661d..2701423d3 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -160,6 +160,7 @@ namespace mlirgen if (option != strictNull->second.front().end()) { compileOptions.strictNullChecks = option->second._arg.value == S("true"); + mth.setStrictNullChecks(compileOptions.strictNullChecks); } } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 10d66dbe4..e6d8c70b0 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -383,6 +383,9 @@ tslang_add_test(NAME test-compile-00-union-bin-ops-2 COMMAND test-runner "${PROJ tslang_add_test(NAME test-compile-00-union-to-any COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_to_any.ts") tslang_add_test(NAME test-compile-00-union-errors COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_errors.ts") tslang_add_test(NAME test-compile-00-union-null-to-string COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_to_string.ts") +tslang_add_test(NAME test-compile-00-union-null-undefined COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts") +tslang_add_test(NAME test-compile-00-union-null-undefined-nonstrict COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined_nonstrict.ts") +tslang_add_test(NAME test-compile-00-optional-number-compare COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.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") tslang_add_test(NAME test-compile-00-length COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00length.ts") @@ -849,6 +852,9 @@ tslang_add_test(NAME test-jit-00-union-bin-ops-2 COMMAND test-runner -jit "${PRO tslang_add_test(NAME test-jit-00-union-to-any COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_to_any.ts") tslang_add_test(NAME test-jit-00-union-errors COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_errors.ts") tslang_add_test(NAME test-jit-00-union-null-to-string COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_to_string.ts") +tslang_add_test(NAME test-jit-00-union-null-undefined COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined.ts") +tslang_add_test(NAME test-jit-00-union-null-undefined-nonstrict COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00union_null_undefined_nonstrict.ts") +tslang_add_test(NAME test-jit-00-optional-number-compare COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00optional_number_compare.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") tslang_add_test(NAME test-jit-00-intersection-type-generic COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00intersection_type_generic.ts") @@ -2460,7 +2466,7 @@ add_test(NAME test-compile-lowering-error-message COMMAND $ --emit=obj --no-default-lib -mm=none "${lowering_error_file}" -o "${CMAKE_CURRENT_BINARY_DIR}/lowering-error-message.obj") set_tests_properties(test-compile-lowering-error-message - PROPERTIES PASS_REGULAR_EXPRESSION "source array and destination array have different types" + PROPERTIES PASS_REGULAR_EXPRESSION "cast from array type .* to array type .* is not supported" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") add_test(NAME test-compile-lowering-error-exit COMMAND $ --emit=obj --no-default-lib -mm=none "${lowering_error_file}" diff --git a/tslang/test/tester/lowering-errors/array_literal_element_type.ts b/tslang/test/tester/lowering-errors/array_literal_element_type.ts index 7d87efc35..e6ae4f3c0 100644 --- a/tslang/test/tester/lowering-errors/array_literal_element_type.ts +++ b/tslang/test/tester/lowering-errors/array_literal_element_type.ts @@ -1,8 +1,12 @@ -// Lowering cannot build this array literal as a number[] | null, and says so with an error. That -// error has to stop the compile: before it did, the object file was written and the program run. +// Lowering cannot read the array out of a number[] | null, and says so with an error. That error has +// to stop the compile: before it did, the object file was written and the program run. // (The array-to-string cast this test used first hits llvm_unreachable in a Debug build before it -// reports anything; this error is reported the same way in both.) +// reports anything. Its second, a const array literal stored as a number[] | null, compiles now.) +function lengthOf(p: number[] | null) { + return p ? p.length : -1; +} + function main() { - let a: number[] | null = [1, 2]; + print(lengthOf(null)); print("done."); } diff --git a/tslang/test/tester/tests/00optional_number_compare.ts b/tslang/test/tester/tests/00optional_number_compare.ts new file mode 100644 index 000000000..fa2cd3a88 --- /dev/null +++ b/tslang/test/tester/tests/00optional_number_compare.ts @@ -0,0 +1,56 @@ +// An optional number compares as the wider of its own type and the other operand's - not as the +// other operand's alone, which made 0.5 an s32 against an integer literal - and only when it holds a +// value: an empty one is not 0, it is less than, greater than and equal to nothing +function holdsFraction() { + let n: number | undefined = 0.5; + assert(!(n === 0), "0.5 === 0"); + assert(n === 0.5, "0.5 === 0.5"); + assert(n !== 0, "0.5 !== 0"); + assert(n < 1, "0.5 < 1"); + assert(n > 0, "0.5 > 0"); + assert(!(0 === n), "0 === 0.5"); + assert(0.5 == n, "0.5 == 0.5"); + + let zero: number = 0; + let half: number = 0.5; + assert(!(n === zero), "0.5 === zero"); + assert(n === half, "0.5 === half"); + assert(n > zero, "0.5 > zero"); +} + +function empty() { + let n: number | undefined = undefined; + assert(!(n === 0), "undefined === 0"); + assert(!(n == 0), "undefined == 0"); + assert(n !== 0, "undefined !== 0"); + assert(!(n < 1), "undefined < 1"); + assert(!(n >= 0), "undefined >= 0"); + assert(!(0 === n), "0 === undefined"); + + let zero: number = 0; + assert(!(n === zero), "undefined === zero"); + assert(!(n <= zero), "undefined <= zero"); +} + +function integers() { + let i: s32 | undefined = 3; + assert(i === 3, "s32 3 === 3"); + assert(!(i === 0), "s32 3 === 0"); + assert(i > 2, "s32 3 > 2"); + assert(i < 3.5, "s32 3 < 3.5"); + + i = undefined; + assert(!(i === 0), "s32 undefined === 0"); + assert(!(i < 1), "s32 undefined < 1"); + + let f: number | undefined = 3; + assert(f === 3, "3.0 === 3"); + assert(f > 2.5, "3.0 > 2.5"); +} + +function main() { + holdsFraction(); + empty(); + integers(); + print("done."); +} diff --git a/tslang/test/tester/tests/00union_null_undefined.ts b/tslang/test/tester/tests/00union_null_undefined.ts new file mode 100644 index 000000000..5eca0b9ed --- /dev/null +++ b/tslang/test/tester/tests/00union_null_undefined.ts @@ -0,0 +1,128 @@ +// `T | null | undefined` accepts null as `T | null` does: undefined makes it optional, it must not take +// the null away - for a nullable T (string, a class), whose null is only there for the type checker, +// and for a value T (number), whose null is a real member of the union +class C { + x = 1; +} + +function isNullString(p: string | null | undefined) { + return p === null; +} + +function isUndefinedString(p: string | null | undefined) { + return p === undefined; +} + +function nullableString() { + let s: string | null | undefined = null; + assert(s === null, "string: null is null"); + assert(s !== undefined, "string: null is not undefined"); + + s = undefined; + assert(s === undefined, "string: undefined is undefined"); + assert(s !== null, "string: undefined is not null"); + + s = "a"; + assert(s === "a", "string: value"); + assert(s !== null && s !== undefined, "string: value is neither"); + + let r: undefined | null | string = null; + assert(r === null, "string, other order: null is null"); + + assert(isNullString(null), "string argument: null"); + assert(!isNullString(undefined), "string argument: undefined is not null"); + assert(isUndefinedString(undefined), "string argument: undefined"); + assert(!isUndefinedString(null), "string argument: null is not undefined"); + assert(!isNullString("b"), "string argument: value"); +} + +function isNullClass(p: C | null | undefined) { + return p === null; +} + +function nullableClass() { + let c: C | null | undefined = null; + assert(c === null, "class: null is null"); + assert(c !== undefined, "class: null is not undefined"); + + c = undefined; + assert(c === undefined, "class: undefined is undefined"); + + c = new C(); + assert(c !== null && c !== undefined, "class: value is neither"); + assert(c.x == 1, "class: value field"); + + assert(isNullClass(null), "class argument: null"); + assert(!isNullClass(undefined), "class argument: undefined is not null"); +} + +function isNullNumber(p: number | null | undefined) { + return p === null; +} + +function valueNumber() { + let n: number | null | undefined = null; + assert(n === null, "number: null is null"); + assert(n !== undefined, "number: null is not undefined"); + + n = undefined; + assert(n === undefined, "number: undefined is undefined"); + + n = 2; + assert(n !== null && n !== undefined, "number: value is neither"); + + assert(isNullNumber(null), "number argument: null"); + assert(!isNullNumber(undefined), "number argument: undefined is not null"); + assert(!isNullNumber(3), "number argument: value"); +} + +// an optional holding undefined equals nothing - loosely, it equals null +function emptyOptional() { + let o: string | undefined = undefined; + assert(o !== null, "empty optional: not strictly null"); + assert(o == null, "empty optional: loosely null"); + assert(!(o != null), "empty optional: not loosely other than null"); + assert(o !== "", "empty optional: not an empty string"); + + let b: boolean | undefined = undefined; + assert(b !== false, "empty optional: not false"); +} + +interface I { + k: number; +} + +class B { + k: number = 2; +} + +function keyOf(p: I | null | undefined) { + return p ? p.k : -1; +} + +function nullableKeyOf(p: I | null) { + return p ? p.k : -1; +} + +// what `T | null` takes is cast to T first: an object literal to an interface, a const array to an array +function castToNullable() { + assert(keyOf({ k: 1 }) == 1, "object literal to optional nullable interface"); + assert(keyOf(null) == -1, "null to optional nullable interface"); + assert(keyOf(undefined) == -1, "undefined to optional nullable interface"); + assert(nullableKeyOf({ k: 3 }) == 3, "object literal to nullable interface"); + assert(nullableKeyOf(new B()) == 2, "class to nullable interface"); + + let a: number[] | null = [1, 2]; + assert(a !== null, "const array to nullable array"); + a = null; + assert(a === null, "null to nullable array"); +} + +function main() { + nullableString(); + nullableClass(); + valueNumber(); + emptyOptional(); + castToNullable(); + print("done."); +} diff --git a/tslang/test/tester/tests/00union_null_undefined_nonstrict.ts b/tslang/test/tester/tests/00union_null_undefined_nonstrict.ts new file mode 100644 index 000000000..d937c097b --- /dev/null +++ b/tslang/test/tester/tests/00union_null_undefined_nonstrict.ts @@ -0,0 +1,54 @@ +// @strict-null false +// Without strict null checks a nullable type (string, a class) absorbs null: its pointer holds it. A +// union with a nullable member is no pointer, though, so there null has to stay a member of its own +class C { + x = 1; +} + +function isNullUnion(p: string | number | null) { + return p === null; +} + +function unionWithNullableMember() { + let u: string | number | null = null; + assert(u === null, "union: null is null"); + + u = "a"; + assert(u === "a", "union: string"); + + u = 1; + assert(u === 1, "union: number"); + + assert(isNullUnion(null), "union argument: null"); + assert(!isNullUnion("b"), "union argument: string"); + assert(!isNullUnion(2), "union argument: number"); + + let v: C | number | null = null; + assert(v === null, "class union: null is null"); + v = 3; + assert(v !== null, "class union: number is not null"); +} + +function nullableOnly() { + let s: string | null = null; + assert(s === null, "string: null"); + + let o: string | null | undefined = null; + assert(o === null, "optional string: null"); + o = undefined; + assert(o === undefined, "optional string: undefined"); + + let c: C | null | undefined = null; + assert(c === null, "optional class: null"); + + let n: number | null | undefined = null; + assert(n === null, "optional number: null"); + n = undefined; + assert(n === undefined, "optional number: undefined"); +} + +function main() { + unionWithNullableMember(); + nullableOnly(); + print("done."); +}