Commit 3ec10365 authored by Joshua Litt's avatar Joshua Litt Committed by Commit Bot

[nullish] Add support for nullish operator

This CL implements the nullish operator in bytecode as defined by:
https://github.com/tc39/proposal-nullish-coalescing. It can be
enabled by passing '--harmony-nullish'.

Nullish is similar to logical operators, but instead of truthy/falsey
values, it short circuits when it evaluates a null or undefined value.


Bug: v8:9547
Change-Id: Ia0f55877fc2714482b5547942baef9733537d1b9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1738568Reviewed-by: 's avatarSathya Gunasekaran  <gsathya@chromium.org>
Reviewed-by: 's avatarLeszek Swirski <leszeks@chromium.org>
Commit-Queue: Joshua Litt <joshualitt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#63317}
parent 882f8c6b
......@@ -122,6 +122,10 @@ bool Expression::IsUndefinedLiteral() const {
var_proxy->raw_name()->IsOneByteEqualTo("undefined");
}
bool Expression::IsLiteralButNotNullOrUndefined() const {
return IsLiteral() && !IsNullOrUndefinedLiteral();
}
bool Expression::ToBooleanIsTrue() const {
return IsLiteral() && AsLiteral()->ToBooleanIsTrue();
}
......
......@@ -246,6 +246,14 @@ class Expression : public AstNode {
// that this also checks for loads of the global "undefined" variable.
bool IsUndefinedLiteral() const;
// True if either null literal or undefined literal.
inline bool IsNullOrUndefinedLiteral() const {
return IsNullLiteral() || IsUndefinedLiteral();
}
// True if a literal and not null or undefined.
bool IsLiteralButNotNullOrUndefined() const;
bool IsCompileTimeValue();
bool IsPattern() {
......
......@@ -208,7 +208,8 @@ DEFINE_IMPLICATION(harmony_import_meta, harmony_dynamic_import)
V(harmony_regexp_sequence, "RegExp Unicode sequence properties") \
V(harmony_weak_refs, "harmony weak references") \
V(harmony_optional_chaining, "harmony optional chaining syntax") \
V(harmony_regexp_match_indices, "harmony regexp match indices")
V(harmony_regexp_match_indices, "harmony regexp match indices") \
V(harmony_nullish, "harmony nullish operator")
#ifdef V8_INTL_SUPPORT
#define HARMONY_INPROGRESS(V) \
......
......@@ -4263,6 +4263,7 @@ EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_dynamic_import)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_import_meta)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_regexp_sequence)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_optional_chaining)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_nullish)
#ifdef V8_INTL_SUPPORT
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_intl_add_calendar_numbering_system)
......
......@@ -5111,6 +5111,9 @@ void BytecodeGenerator::VisitBinaryOperation(BinaryOperation* binop) {
case Token::AND:
VisitLogicalAndExpression(binop);
break;
case Token::NULLISH:
VisitNullishExpression(binop);
break;
default:
VisitArithmeticExpression(binop);
break;
......@@ -5128,6 +5131,9 @@ void BytecodeGenerator::VisitNaryOperation(NaryOperation* expr) {
case Token::AND:
VisitNaryLogicalAndExpression(expr);
break;
case Token::NULLISH:
VisitNaryNullishExpression(expr);
break;
default:
VisitNaryArithmeticExpression(expr);
break;
......@@ -5511,14 +5517,16 @@ void BytecodeGenerator::VisitNaryCommaExpression(NaryOperation* expr) {
void BytecodeGenerator::VisitLogicalTestSubExpression(
Token::Value token, Expression* expr, BytecodeLabels* then_labels,
BytecodeLabels* else_labels, int coverage_slot) {
DCHECK(token == Token::OR || token == Token::AND);
DCHECK(token == Token::OR || token == Token::AND || token == Token::NULLISH);
BytecodeLabels test_next(zone());
if (token == Token::OR) {
VisitForTest(expr, then_labels, &test_next, TestFallthrough::kElse);
} else {
DCHECK_EQ(Token::AND, token);
} else if (token == Token::AND) {
VisitForTest(expr, &test_next, else_labels, TestFallthrough::kThen);
} else {
DCHECK_EQ(Token::NULLISH, token);
VisitForNullishTest(expr, then_labels, &test_next, else_labels);
}
test_next.Bind(builder());
......@@ -5528,7 +5536,7 @@ void BytecodeGenerator::VisitLogicalTestSubExpression(
void BytecodeGenerator::VisitLogicalTest(Token::Value token, Expression* left,
Expression* right,
int right_coverage_slot) {
DCHECK(token == Token::OR || token == Token::AND);
DCHECK(token == Token::OR || token == Token::AND || token == Token::NULLISH);
TestResultScope* test_result = execution_result()->AsTest();
BytecodeLabels* then_labels = test_result->then_labels();
BytecodeLabels* else_labels = test_result->else_labels();
......@@ -5543,7 +5551,7 @@ void BytecodeGenerator::VisitLogicalTest(Token::Value token, Expression* left,
void BytecodeGenerator::VisitNaryLogicalTest(
Token::Value token, NaryOperation* expr,
const NaryCodeCoverageSlots* coverage_slots) {
DCHECK(token == Token::OR || token == Token::AND);
DCHECK(token == Token::OR || token == Token::AND || token == Token::NULLISH);
DCHECK_GT(expr->subsequent_length(), 0);
TestResultScope* test_result = execution_result()->AsTest();
......@@ -5599,6 +5607,27 @@ bool BytecodeGenerator::VisitLogicalAndSubExpression(Expression* expr,
return false;
}
bool BytecodeGenerator::VisitNullishSubExpression(Expression* expr,
BytecodeLabels* end_labels,
int coverage_slot) {
if (expr->IsLiteralButNotNullOrUndefined()) {
VisitForAccumulatorValue(expr);
end_labels->Bind(builder());
return true;
} else if (!expr->IsNullOrUndefinedLiteral()) {
VisitForAccumulatorValue(expr);
BytecodeLabel is_null_or_undefined;
builder()
->JumpIfUndefinedOrNull(&is_null_or_undefined)
.Jump(end_labels->New());
builder()->Bind(&is_null_or_undefined);
}
BuildIncrementBlockCoverageCounterIfEnabled(coverage_slot);
return false;
}
void BytecodeGenerator::VisitLogicalOrExpression(BinaryOperation* binop) {
Expression* left = binop->left();
Expression* right = binop->right();
......@@ -5721,6 +5750,68 @@ void BytecodeGenerator::VisitNaryLogicalAndExpression(NaryOperation* expr) {
}
}
void BytecodeGenerator::VisitNullishExpression(BinaryOperation* binop) {
Expression* left = binop->left();
Expression* right = binop->right();
int right_coverage_slot =
AllocateBlockCoverageSlotIfEnabled(binop, SourceRangeKind::kRight);
if (execution_result()->IsTest()) {
TestResultScope* test_result = execution_result()->AsTest();
if (left->IsLiteralButNotNullOrUndefined() && left->ToBooleanIsTrue()) {
builder()->Jump(test_result->NewThenLabel());
} else if (left->IsNullOrUndefinedLiteral() &&
right->IsNullOrUndefinedLiteral()) {
BuildIncrementBlockCoverageCounterIfEnabled(right_coverage_slot);
builder()->Jump(test_result->NewElseLabel());
} else {
VisitLogicalTest(Token::NULLISH, left, right, right_coverage_slot);
}
test_result->SetResultConsumedByTest();
} else {
BytecodeLabels end_labels(zone());
if (VisitNullishSubExpression(left, &end_labels, right_coverage_slot)) {
return;
}
VisitForAccumulatorValue(right);
end_labels.Bind(builder());
}
}
void BytecodeGenerator::VisitNaryNullishExpression(NaryOperation* expr) {
Expression* first = expr->first();
DCHECK_GT(expr->subsequent_length(), 0);
NaryCodeCoverageSlots coverage_slots(this, expr);
if (execution_result()->IsTest()) {
TestResultScope* test_result = execution_result()->AsTest();
if (first->IsLiteralButNotNullOrUndefined() && first->ToBooleanIsTrue()) {
builder()->Jump(test_result->NewThenLabel());
} else {
VisitNaryLogicalTest(Token::NULLISH, expr, &coverage_slots);
}
test_result->SetResultConsumedByTest();
} else {
BytecodeLabels end_labels(zone());
if (VisitNullishSubExpression(first, &end_labels,
coverage_slots.GetSlotFor(0))) {
return;
}
for (size_t i = 0; i < expr->subsequent_length() - 1; ++i) {
if (VisitNullishSubExpression(expr->subsequent(i), &end_labels,
coverage_slots.GetSlotFor(i + 1))) {
return;
}
}
// We have to visit the last value even if it's nullish, because we need its
// actual value.
VisitForAccumulatorValue(expr->subsequent(expr->subsequent_length() - 1));
end_labels.Bind(builder());
}
}
void BytecodeGenerator::BuildNewLocalActivationContext() {
ValueResultScope value_execution_result(this);
Scope* scope = closure_scope();
......@@ -6066,6 +6157,25 @@ void BytecodeGenerator::VisitForTest(Expression* expr,
}
}
// Visits the expression |expr| for testing its nullish value and jumping to the
// |then| or |other| label depending on value and short-circuit semantics
void BytecodeGenerator::VisitForNullishTest(Expression* expr,
BytecodeLabels* then_labels,
BytecodeLabels* test_next_labels,
BytecodeLabels* else_labels) {
// Nullish short circuits on undefined or null, otherwise we fall back to
// BuildTest with no fallthrough.
// TODO(joshualitt): We should do this in a TestResultScope.
TypeHint type_hint = VisitForAccumulatorValue(expr);
ToBooleanMode mode = ToBooleanModeFromTypeHint(type_hint);
// Skip the nullish shortcircuit if we already have a boolean.
if (mode != ToBooleanMode::kAlreadyBoolean) {
builder()->JumpIfUndefinedOrNull(test_next_labels->New());
}
BuildTest(mode, then_labels, else_labels, TestFallthrough::kNone);
}
void BytecodeGenerator::VisitInSameTestExecutionScope(Expression* expr) {
DCHECK(execution_result()->IsTest());
{
......
......@@ -162,12 +162,14 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
void VisitCommaExpression(BinaryOperation* binop);
void VisitLogicalOrExpression(BinaryOperation* binop);
void VisitLogicalAndExpression(BinaryOperation* binop);
void VisitNullishExpression(BinaryOperation* binop);
// Dispatched from VisitNaryOperation.
void VisitNaryArithmeticExpression(NaryOperation* expr);
void VisitNaryCommaExpression(NaryOperation* expr);
void VisitNaryLogicalOrExpression(NaryOperation* expr);
void VisitNaryLogicalAndExpression(NaryOperation* expr);
void VisitNaryNullishExpression(NaryOperation* expr);
// Dispatched from VisitUnaryOperation.
void VisitVoid(UnaryOperation* expr);
......@@ -321,6 +323,7 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
int right_coverage_slot);
void VisitNaryLogicalTest(Token::Value token, NaryOperation* expr,
const NaryCodeCoverageSlots* coverage_slots);
// Visit a (non-RHS) test for a logical op, which falls through if the test
// fails or jumps to the appropriate labels if it succeeds.
void VisitLogicalTestSubExpression(Token::Value token, Expression* expr,
......@@ -335,6 +338,10 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
BytecodeLabels* end_labels,
int coverage_slot);
// Helper for binary and nary nullish op value expressions.
bool VisitNullishSubExpression(Expression* expr, BytecodeLabels* end_labels,
int coverage_slot);
// Visit the body of a loop iteration.
void VisitIterationBody(IterationStatement* stmt, LoopBuilder* loop_builder);
......@@ -376,6 +383,9 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
void VisitForEffect(Expression* expr);
void VisitForTest(Expression* expr, BytecodeLabels* then_labels,
BytecodeLabels* else_labels, TestFallthrough fallthrough);
void VisitForNullishTest(Expression* expr, BytecodeLabels* then_labels,
BytecodeLabels* test_next_labels,
BytecodeLabels* else_labels);
void VisitInSameTestExecutionScope(Expression* expr);
......
......@@ -63,6 +63,7 @@ ParseInfo::ParseInfo(Isolate* isolate, AccountingAllocator* zone_allocator)
set_allow_harmony_dynamic_import(FLAG_harmony_dynamic_import);
set_allow_harmony_import_meta(FLAG_harmony_import_meta);
set_allow_harmony_optional_chaining(FLAG_harmony_optional_chaining);
set_allow_harmony_nullish(FLAG_harmony_nullish);
set_allow_harmony_private_methods(FLAG_harmony_private_methods);
}
......
......@@ -112,6 +112,9 @@ class V8_EXPORT_PRIVATE ParseInfo {
FLAG_ACCESSOR(kIsOneshotIIFE, is_oneshot_iife, set_is_oneshot_iife)
FLAG_ACCESSOR(kCollectSourcePositions, collect_source_positions,
set_collect_source_positions)
FLAG_ACCESSOR(kAllowHarmonyNullish, allow_harmony_nullish,
set_allow_harmony_nullish)
#undef FLAG_ACCESSOR
void set_parse_restriction(ParseRestriction restriction) {
......@@ -277,7 +280,7 @@ class V8_EXPORT_PRIVATE ParseInfo {
void SetFunctionInfo(T function);
// Various configuration flags for parsing.
enum Flag {
enum Flag : uint64_t {
// ---------- Input flags ---------------------------
kToplevel = 1 << 0,
kEager = 1 << 1,
......@@ -310,12 +313,13 @@ class V8_EXPORT_PRIVATE ParseInfo {
kAllowHarmonyPrivateFields = 1 << 28,
kAllowHarmonyPrivateMethods = 1 << 29,
kIsOneshotIIFE = 1 << 30,
kCollectSourcePositions = 1 << 31,
kCollectSourcePositions = static_cast<uint64_t>(1) << 31,
kAllowHarmonyNullish = static_cast<uint64_t>(1) << 32,
};
//------------- Inputs to parsing and scope analysis -----------------------
std::unique_ptr<Zone> zone_;
unsigned flags_;
uint64_t flags_;
v8::Extension* extension_;
DeclarationScope* script_scope_;
uintptr_t stack_limit_;
......
......@@ -294,6 +294,14 @@ class ParserBase {
scanner()->set_allow_harmony_optional_chaining(allow);
}
bool allow_harmony_nullish() const {
return scanner()->allow_harmony_nullish();
}
void set_allow_harmony_nullish(bool allow) {
scanner()->set_allow_harmony_nullish(allow);
}
uintptr_t stack_limit() const { return stack_limit_; }
void set_stack_limit(uintptr_t stack_limit) { stack_limit_ = stack_limit; }
......@@ -1059,6 +1067,8 @@ class ParserBase {
ExpressionT ParseYieldExpression();
V8_INLINE ExpressionT ParseConditionalExpression();
ExpressionT ParseConditionalContinuation(ExpressionT expression, int pos);
ExpressionT ParseLogicalExpression();
ExpressionT ParseCoalesceExpression(ExpressionT expression);
ExpressionT ParseBinaryContinuation(ExpressionT x, int prec, int prec1);
V8_INLINE ExpressionT ParseBinaryExpression(int prec);
ExpressionT ParseUnaryOrPrefixExpression();
......@@ -2800,17 +2810,70 @@ template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseConditionalExpression() {
// ConditionalExpression ::
// LogicalOrExpression
// LogicalOrExpression '?' AssignmentExpression ':' AssignmentExpression
// LogicalExpression
// LogicalExpression '?' AssignmentExpression ':' AssignmentExpression
//
int pos = peek_position();
// We start using the binary expression parser for prec >= 4 only!
ExpressionT expression = ParseBinaryExpression(4);
ExpressionT expression = ParseLogicalExpression();
return peek() == Token::CONDITIONAL
? ParseConditionalContinuation(expression, pos)
: expression;
}
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseLogicalExpression() {
// LogicalExpression ::
// LogicalORExpression
// CoalesceExpression
// Both LogicalORExpression and CoalesceExpression start with BitwiseOR.
// Parse for binary expressions >= 6 (BitwiseOR);
ExpressionT expression = ParseBinaryExpression(6);
if (peek() == Token::AND || peek() == Token::OR) {
// LogicalORExpression, pickup parsing where we left off.
int prec1 = Token::Precedence(peek(), accept_IN_);
expression = ParseBinaryContinuation(expression, 4, prec1);
} else if (V8_UNLIKELY(peek() == Token::NULLISH)) {
expression = ParseCoalesceExpression(expression);
}
return expression;
}
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseCoalesceExpression(ExpressionT expression) {
// CoalesceExpression ::
// CoalesceExpressionHead ?? BitwiseORExpression
//
// CoalesceExpressionHead ::
// CoalesceExpression
// BitwiseORExpression
// We create a binary operation for the first nullish, otherwise collapse
// into an nary expresion.
bool first_nullish = true;
while (peek() == Token::NULLISH) {
SourceRange right_range;
SourceRangeScope right_range_scope(scanner(), &right_range);
Consume(Token::NULLISH);
int pos = peek_position();
// Parse BitwiseOR or higher.
ExpressionT y = ParseBinaryExpression(6);
if (first_nullish) {
expression =
factory()->NewBinaryOperation(Token::NULLISH, expression, y, pos);
impl()->RecordBinaryOperationSourceRange(expression, right_range);
first_nullish = false;
} else {
impl()->CollapseNaryExpression(&expression, y, Token::NULLISH, pos,
right_range);
}
}
return expression;
}
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseConditionalContinuation(ExpressionT expression,
......
......@@ -424,6 +424,7 @@ Parser::Parser(ParseInfo* info)
set_allow_natives(info->allow_natives_syntax());
set_allow_harmony_dynamic_import(info->allow_harmony_dynamic_import());
set_allow_harmony_import_meta(info->allow_harmony_import_meta());
set_allow_harmony_nullish(info->allow_harmony_nullish());
set_allow_harmony_optional_chaining(info->allow_harmony_optional_chaining());
set_allow_harmony_private_methods(info->allow_harmony_private_methods());
for (int feature = 0; feature < v8::Isolate::kUseCounterFeatureCount;
......
......@@ -363,12 +363,14 @@ V8_INLINE Token::Value Scanner::ScanSingleToken() {
return Select(token);
case Token::CONDITIONAL:
// ? ?.
// ? ?. ??
Advance();
if (allow_harmony_optional_chaining() && c0_ == '.') {
Advance();
if (!IsDecimalDigit(c0_)) return Token::QUESTION_PERIOD;
PushBack('.');
} else if (allow_harmony_nullish() && c0_ == '?') {
return Select(Token::NULLISH);
}
return Token::CONDITIONAL;
......
......@@ -93,6 +93,7 @@ Scanner::Scanner(Utf16CharacterStream* source, bool is_module)
: source_(source),
found_html_comment_(false),
allow_harmony_optional_chaining_(false),
allow_harmony_nullish_(false),
is_module_(is_module),
octal_pos_(Location::invalid()),
octal_message_(MessageTemplate::kNone) {
......
......@@ -414,6 +414,10 @@ class V8_EXPORT_PRIVATE Scanner {
allow_harmony_optional_chaining_ = allow;
}
bool allow_harmony_nullish() const { return allow_harmony_nullish_; }
void set_allow_harmony_nullish(bool allow) { allow_harmony_nullish_ = allow; }
const Utf16CharacterStream* stream() const { return source_; }
// If the next characters in the stream are "#!", the line is skipped.
......@@ -723,6 +727,7 @@ class V8_EXPORT_PRIVATE Scanner {
// Harmony flags to allow ESNext features.
bool allow_harmony_optional_chaining_;
bool allow_harmony_nullish_;
const bool is_module_;
......
......@@ -96,6 +96,7 @@ namespace internal {
/* IsBinaryOp() relies on this block of enum values */ \
/* being contiguous and sorted in the same order! */ \
T(COMMA, ",", 1) \
T(NULLISH, "??", 3) \
T(OR, "||", 4) \
T(AND, "&&", 5) \
\
......
......@@ -261,6 +261,7 @@ TEST(ArrowOrAssignmentOp) {
bool TokenIsBinaryOp(Token::Value token) {
switch (token) {
case Token::COMMA:
case Token::NULLISH:
case Token::OR:
case Token::AND:
#define T(name, string, precedence) case Token::name:
......@@ -1530,6 +1531,7 @@ enum ParserFlag {
kAllowHarmonyPrivateMethods,
kAllowHarmonyDynamicImport,
kAllowHarmonyImportMeta,
kAllowHarmonyNullish,
kAllowHarmonyOptionalChaining,
};
......@@ -1546,6 +1548,7 @@ void SetGlobalFlags(base::EnumSet<ParserFlag> flags) {
i::FLAG_harmony_import_meta = flags.contains(kAllowHarmonyImportMeta);
i::FLAG_harmony_optional_chaining =
flags.contains(kAllowHarmonyOptionalChaining);
i::FLAG_harmony_nullish = flags.contains(kAllowHarmonyNullish);
}
void SetParserFlags(i::PreParser* parser, base::EnumSet<ParserFlag> flags) {
......@@ -1558,6 +1561,7 @@ void SetParserFlags(i::PreParser* parser, base::EnumSet<ParserFlag> flags) {
flags.contains(kAllowHarmonyImportMeta));
parser->set_allow_harmony_optional_chaining(
flags.contains(kAllowHarmonyOptionalChaining));
parser->set_allow_harmony_nullish(flags.contains(kAllowHarmonyNullish));
}
void TestParserSyncWithFlags(i::Handle<i::String> source,
......@@ -2009,6 +2013,41 @@ TEST(OptionalChainingTaggedError) {
RunParserSyncTest(context_data, statement_data, kError);
}
TEST(Nullish) {
v8::HandleScope handles(CcTest::isolate());
v8::Local<v8::Context> context = v8::Context::New(CcTest::isolate());
v8::Context::Scope context_scope(context);
const char* context_data[][2] = {
{"", ""}, {"'use strict';", ""}, {nullptr, nullptr}};
const char* statement_data[] = {"a ?? b", "a ?? b ?? c",
"a ?? b ? c : d"
"a ?? b ?? c ? d : e",
nullptr};
static const ParserFlag flags[] = {kAllowHarmonyNullish};
RunParserSyncTest(context_data, statement_data, kSuccess, nullptr, 0, flags,
1, nullptr, 0, false, true, true);
RunParserSyncTest(context_data, statement_data, kError);
}
TEST(NullishNotContained) {
v8::HandleScope handles(CcTest::isolate());
v8::Local<v8::Context> context = v8::Context::New(CcTest::isolate());
v8::Context::Scope context_scope(context);
const char* context_data[][2] = {
{"", ""}, {"'use strict';", ""}, {nullptr, nullptr}};
const char* statement_data[] = {"a || b ?? c", "a ?? b || c",
"a && b ?? c"
"a ?? b && c",
nullptr};
static const ParserFlag flags[] = {kAllowHarmonyNullish};
RunParserSyncTest(context_data, statement_data, kError, nullptr, 0, flags, 1,
nullptr, 0, false, true, true);
}
TEST(ErrorsEvalAndArguments) {
// Tests that both preparsing and parsing produce the right kind of errors for
// using "eval" and "arguments" as identifiers. Without the strict mode, it's
......
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Flags: --harmony-nullish
// Basic sanity checks.
assertTrue(true ?? false);
assertFalse(false ?? true);
assertTrue(undefined ?? true);
assertTrue(null ?? true);
assertEquals([], [] ?? true);
// Chaining nullish.
assertTrue(null ?? null ?? true);
assertTrue(null ?? null ?? true ?? null);
assertTrue(undefined ?? undefined ?? true);
assertTrue(undefined ?? undefined ?? true ?? undefined);
assertFalse(null ?? false ?? null);
assertFalse(undefined ?? false ?? undefined);
// Nullish and conditionals.
assertTrue(null ?? true ? true : false);
assertTrue(null ?? null ?? true ? true : false);
assertTrue(undefined ?? true ? true : false);
assertTrue(undefined ?? undefined ?? true ? true : false);
// Nullish mixed expressions.
assertTrue(null ?? 1 == 1);
assertTrue(undefined ?? 1 == 1);
assertTrue(null ?? null ?? 1 == 1);
assertTrue(undefined ??undefined ?? 1 == 1);
assertEquals(1, null ?? 1 | 0);
assertEquals(1, undefined ?? 1 | 0);
assertEquals(1, null ?? null ?? 1 | 0);
assertEquals(1, undefined ?? undefined ?? 1 | 0);
// Short circuit.
{
let ran = false;
let never_ran = () => { ran = true; }
let value = true ?? never_ran();
assertTrue(value);
assertFalse(ran);
}
{
let ran = false;
let never_ran = () => { ran = true; }
let value = undefined ?? true ?? never_ran();
assertTrue(value);
assertFalse(ran);
}
{
let ran = false;
let never_ran = () => { ran = true; }
let value = null ?? true ?? never_ran();
assertTrue(value);
assertFalse(ran);
}
// Nullish in tests evaluates only once.
{
let run_count = 0;
let run = () => { run_count++; return null; }
if (run() ?? true) {} else { assertUnreachable(); }
assertEquals(1, run_count);
}
// Nullish may not contain or be contained within || or &&.
assertThrows("true || true ?? true", SyntaxError);
assertThrows("true ?? true || true", SyntaxError);
assertThrows("true && true ?? true", SyntaxError);
assertThrows("true ?? true && true", SyntaxError);
// Test boolean expressions and nullish.
assertTrue((false || true) ?? false);
assertTrue(null ?? (false || true));
assertTrue((false || null) ?? true);
assertTrue((false || null) ?? (true && null) ?? true);
assertTrue((false || undefined) ?? true);
assertTrue((false || undefined) ?? (true && undefined) ?? true);
assertTrue(null ?? (false || true));
assertTrue(undefined ?? (false || true));
assertTrue(null ?? (false || null) ?? true);
assertTrue(undefined ?? (false || undefined) ?? true);
assertTrue(null ?? null ?? (false || true));
assertTrue(undefined ?? undefined ?? (false || true));
assertTrue((undefined ?? false) || true);
assertTrue((null ?? false) || true);
assertTrue((undefined ?? undefined ?? false) || false || true);
assertTrue((null ?? null ?? false) || false || true);
assertTrue(false || (undefined ?? true));
assertTrue(false || (null ?? true));
assertTrue(false || false || (undefined ?? undefined ?? true));
assertTrue(false || false || (null ?? null ?? true));
// Test use when test true.
if (undefined ?? true) {} else { assertUnreachable(); }
if (null ?? true) {} else { assertUnreachable(); }
if (undefined ?? undefined ?? true) {} else { assertUnreachable(); }
if (null ?? null ?? true) {} else { assertUnreachable(); }
// test use when test false
if (undefined ?? false) { assertUnreachable(); } else {}
if (null ?? false) { assertUnreachable(); } else {}
if (undefined ?? undefined ?? false) { assertUnreachable(); } else {}
if (null ?? null ?? false) { assertUnreachable(); } else {}
if (undefined ?? false ?? true) { assertUnreachable(); } else {}
if (null ?? false ?? true) { assertUnreachable(); } else {}
// Test use with nested boolean.
if ((false || undefined) ?? true) {} else { assertUnreachable(); }
if ((false || null) ?? true) {} else { assertUnreachable(); }
if ((false || undefined) ?? undefined ?? true) {} else { assertUnreachable(); }
if ((false || null) ?? null ?? true) {} else { assertUnreachable(); }
if (undefined ?? (false || true)) {} else { assertUnreachable(); }
if (null ?? (false || true)) {} else { assertUnreachable(); }
if (undefined ?? undefined ?? (false || true)) {} else { assertUnreachable(); }
if (null ?? null ?? (false || true)) {} else { assertUnreachable(); }
if (undefined ?? (false || undefined) ?? true) {} else { assertUnreachable(); }
if (null ?? (false || null) ?? true) {} else { assertUnreachable(); }
// Nested nullish.
if ((null ?? true) || false) {} else { assertUnreachable(); }
if ((null ?? null ?? true) || false) {} else { assertUnreachable(); }
if (false || (null ?? true)) {} else { assertUnreachable(); }
if (false || (null ?? null ?? true)) {} else { assertUnreachable(); }
if ((null ?? false) || false) { assertUnreachable(); } else {}
if ((null ?? null ?? false) || false) { assertUnreachable(); } else {}
if (false || (null ?? false)) { assertUnreachable(); } else {}
if (false || (null ?? null ?? false)) { assertUnreachable(); } else {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment