mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibJS: Add a JS::Completion class and JS::ThrowCompletionOr<T> template
We decided that we want to move away from throwing exceptions in AOs and regular functions implicitly and then returning some default-constructed value (usually an empty JS::Value) - this requires remembering to check for an exception at the call site, which is error-prone. It's also awkward for return values that cannot be default-constructed, e.g. MarkedValueList. Instead, the thrown value should be part of the function return value. The solution to this is moving closer to the spec and using something they call "completion records": https://tc39.es/ecma262/#sec-completion-record-specification-type This has various advantages: - It becomes crystal clear whether some AO can fail or not, and errors need to be handled and values unwrapped explicitly (for that reason compatibility with the TRY() macro is already built-in, and a similar TRY_OR_DISCARD() macro has been added specifically for use in LibJS, while the majority of functions doesn't return ThrowCompletionOr yet) - We no longer need to mix "valid" and "invalid" values of various types for the success and exception outcomes (e.g. null/non-null AK::String, empty/non-empty JS::Value) - Subsequently it's no longer possible to accidentally use an exception outcome return value as a success outcome return value (e.g. any AO that returns a numeric type would return 0 even after throwing an exception, at least before we started making use of Optional for that) - Eventually the VM will no longer need to store an exception, and temporarily clearing an exception (e.g. to call a function) becomes obsolete - instead, completions will simply propagate up to the caller outside of LibJS, which then can deal with it in any way - Similar to throw we'll be able to implement the functionality of break, continue, and return using completions, which will lead to easier to understand code and fewer workarounds - the current unwinding mechanism is not even remotely similar to the spec's approach The spec's NormalCompletion and ThrowCompletion AOs have been implemented as simple wrappers around the JS::Completion constructor. UpdateEmpty has been implemented as a JS::Completion method. There's also a new VM::throw_completion<T>() helper, which basically works like VM::throw_exception<T>() - it creates a T object (usually a JS::Error), and returns it wrapped in a JS::Completion of Type::Throw. Two temporary usage patterns have emerged: 1. Callee already returns ThrowCompletionOr, but caller doesn't: auto foo = TRY_OR_DISCARD(bar()); 2. Caller already returns ThrowCompletionOr, but callee doesn't: auto foo = bar(); if (auto* exception = vm.exception()) return throw_completion(exception->value()); Eventually all error handling and unwrapping can be done with just TRY() or possibly even operator? in the future :^) Co-authored-by: Andreas Kling <kling@serenityos.org>
This commit is contained in:
parent
cbdd069279
commit
33679a8445
Notes:
sideshowbarker
2024-07-18 03:54:38 +09:00
Author: https://github.com/linusg Commit: https://github.com/SerenityOS/serenity/commit/33679a8445b Pull-request: https://github.com/SerenityOS/serenity/pull/10046 Reviewed-by: https://github.com/IdanHo Reviewed-by: https://github.com/awesomekling
3 changed files with 166 additions and 0 deletions
|
@ -141,6 +141,7 @@ class BoundFunction;
|
|||
class Cell;
|
||||
class CellAllocator;
|
||||
class ClassExpression;
|
||||
class Completion;
|
||||
class Console;
|
||||
class DeclarativeEnvironment;
|
||||
class DeferGC;
|
||||
|
@ -225,6 +226,9 @@ JS_ENUMERATE_TEMPORAL_OBJECTS
|
|||
struct TemporalDuration;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class ThrowCompletionOr;
|
||||
|
||||
template<class T>
|
||||
class Handle;
|
||||
|
||||
|
|
151
Userland/Libraries/LibJS/Runtime/Completion.h
Normal file
151
Userland/Libraries/LibJS/Runtime/Completion.h
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/FlyString.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/Try.h>
|
||||
#include <LibJS/Runtime/Value.h>
|
||||
|
||||
namespace JS {
|
||||
|
||||
// Temporary helper akin to TRY(), but returning a default-constructed type (e.g. empty JS::Value)
|
||||
// instead of the throw completion record. Use this as the bridge between functions that have
|
||||
// already been updated to use completions and functions that haven't.
|
||||
#define TRY_OR_DISCARD(expression) \
|
||||
({ \
|
||||
auto _temporary_result = (expression); \
|
||||
if (_temporary_result.is_error()) \
|
||||
return {}; \
|
||||
_temporary_result.release_value(); \
|
||||
})
|
||||
|
||||
// 6.2.3 The Completion Record Specification Type, https://tc39.es/ecma262/#sec-completion-record-specification-type
|
||||
class [[nodiscard]] Completion {
|
||||
public:
|
||||
enum class Type {
|
||||
Normal,
|
||||
Break,
|
||||
Continue,
|
||||
Return,
|
||||
Throw,
|
||||
};
|
||||
|
||||
Completion(Type type, Optional<Value> value, Optional<FlyString> target)
|
||||
: m_type(type)
|
||||
, m_value(move(value))
|
||||
, m_target(move(target))
|
||||
{
|
||||
if (m_value.has_value())
|
||||
VERIFY(!m_value->is_empty());
|
||||
}
|
||||
|
||||
// 5.2.3.1 Implicit Completion Values, https://tc39.es/ecma262/#sec-implicit-completion-values
|
||||
// Not `explicit` on purpose.
|
||||
Completion(Value value)
|
||||
: Completion(Type::Normal, value, {})
|
||||
{
|
||||
}
|
||||
Completion()
|
||||
: Completion(js_undefined())
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] Type type() const { return m_type; }
|
||||
|
||||
[[nodiscard]] bool has_value() const { return m_value.has_value(); }
|
||||
[[nodiscard]] Value value() const { return *m_value; }
|
||||
|
||||
[[nodiscard]] bool has_target() const { return m_target.has_value(); }
|
||||
[[nodiscard]] FlyString const& target() const { return *m_target; }
|
||||
|
||||
// These are for compatibility with the TRY() macro in AK.
|
||||
[[nodiscard]] bool is_error() const { return m_type == Type::Throw; }
|
||||
[[nodiscard]] Value release_value() { return m_value.release_value(); }
|
||||
Completion release_error()
|
||||
{
|
||||
VERIFY(is_error());
|
||||
return { m_type, release_value(), move(m_target) };
|
||||
}
|
||||
|
||||
// 6.2.3.4 UpdateEmpty ( completionRecord, value ), https://tc39.es/ecma262/#sec-updateempty
|
||||
Completion update_empty(Value value) const
|
||||
{
|
||||
// 1. Assert: If completionRecord.[[Type]] is either return or throw, then completionRecord.[[Value]] is not empty.
|
||||
if (m_type == Type::Return || m_type == Type::Throw)
|
||||
VERIFY(m_value.has_value());
|
||||
|
||||
// 2. If completionRecord.[[Value]] is not empty, return Completion(completionRecord).
|
||||
if (m_value.has_value())
|
||||
return *this;
|
||||
|
||||
// 3. Return Completion { [[Type]]: completionRecord.[[Type]], [[Value]]: value, [[Target]]: completionRecord.[[Target]] }.
|
||||
return { m_type, value, m_target };
|
||||
}
|
||||
|
||||
private:
|
||||
Type m_type { Type::Normal }; // [[Type]]
|
||||
Optional<Value> m_value; // [[Value]]
|
||||
Optional<FlyString> m_target; // [[Target]]
|
||||
};
|
||||
|
||||
template<typename ValueType>
|
||||
class [[nodiscard]] ThrowCompletionOr {
|
||||
public:
|
||||
ThrowCompletionOr() requires(IsSame<ValueType, Empty>) = default;
|
||||
|
||||
// Not `explicit` on purpose so that `return vm.throw_completion<Error>(...);` is possible.
|
||||
ThrowCompletionOr(Completion throw_completion)
|
||||
: m_throw_completion(move(throw_completion))
|
||||
{
|
||||
VERIFY(throw_completion.is_error());
|
||||
}
|
||||
|
||||
// Not `explicit` on purpose so that `return value;` is possible.
|
||||
ThrowCompletionOr(ValueType value)
|
||||
: m_value(move(value))
|
||||
{
|
||||
if constexpr (IsSame<ValueType, Value>)
|
||||
VERIFY(!value.is_empty());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_throw_completion() const { return m_throw_completion.has_value(); }
|
||||
Completion const& throw_completion() const { return *m_throw_completion; }
|
||||
|
||||
[[nodiscard]] bool has_value() const requires(!IsSame<ValueType, Empty>) { return m_value.has_value(); }
|
||||
[[nodiscard]] ValueType const& value() const requires(!IsSame<ValueType, Empty>) { return *m_value; }
|
||||
|
||||
// These are for compatibility with the TRY() macro in AK.
|
||||
[[nodiscard]] bool is_error() const { return m_throw_completion.has_value(); }
|
||||
[[nodiscard]] ValueType release_value() { return m_value.release_value(); }
|
||||
Completion release_error() { return m_throw_completion.release_value(); }
|
||||
|
||||
private:
|
||||
Optional<Completion> m_throw_completion;
|
||||
Optional<ValueType> m_value;
|
||||
};
|
||||
|
||||
template<>
|
||||
class ThrowCompletionOr<void> : public ThrowCompletionOr<Empty> {
|
||||
public:
|
||||
using ThrowCompletionOr<Empty>::ThrowCompletionOr;
|
||||
};
|
||||
|
||||
// 6.2.3.2 NormalCompletion ( value ), https://tc39.es/ecma262/#sec-normalcompletion
|
||||
inline Completion normal_completion(Value value)
|
||||
{
|
||||
return { Completion::Type::Normal, value, {} };
|
||||
}
|
||||
|
||||
// 6.2.3.3 ThrowCompletion ( value ), https://tc39.es/ecma262/#sec-throwcompletion
|
||||
inline Completion throw_completion(Value value)
|
||||
{
|
||||
return { Completion::Type::Throw, value, {} };
|
||||
}
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
#include <AK/Variant.h>
|
||||
#include <LibJS/Heap/Heap.h>
|
||||
#include <LibJS/Runtime/CommonPropertyNames.h>
|
||||
#include <LibJS/Runtime/Completion.h>
|
||||
#include <LibJS/Runtime/Error.h>
|
||||
#include <LibJS/Runtime/ErrorTypes.h>
|
||||
#include <LibJS/Runtime/Exception.h>
|
||||
|
@ -231,6 +232,16 @@ public:
|
|||
return throw_exception(global_object, T::create(global_object, String::formatted(type.message(), forward<Args>(args)...)));
|
||||
}
|
||||
|
||||
// 5.2.3.2 Throw an Exception, https://tc39.es/ecma262/#sec-throw-an-exception
|
||||
template<typename T, typename... Args>
|
||||
Completion throw_completion(GlobalObject& global_object, ErrorType type, Args&&... args)
|
||||
{
|
||||
auto* error = T::create(global_object, String::formatted(type.message(), forward<Args>(args)...));
|
||||
// NOTE: This is temporary until we remove VM::exception().
|
||||
throw_exception(global_object, error);
|
||||
return JS::throw_completion(error);
|
||||
}
|
||||
|
||||
Value construct(FunctionObject&, FunctionObject& new_target, Optional<MarkedValueList> arguments);
|
||||
|
||||
String join_arguments(size_t start_index = 0) const;
|
||||
|
|
Loading…
Reference in a new issue