[torque] Add LazyNode support

This change adds a new abstract type Lazy<T> which can be used to
interoperate with CSA code that uses LazyNode. This new type has special
code-generation rules because its generated type is not TNode<...> but
std::function<TNode<...>()>. Torque code can do nothing with this type
except pass it around, but passing it to the CSA function RunLazy is an
easy way to execute the std::function and get back a normal value.
Torque code can also create Lazy<T> values using the intrinsic function
%MakeLazy, which takes the name of a macro as its first parameter,
followed by arguments to that macro which will be passed when the
LazyNode is evaluated. We use the macro's name because the language
doesn't support taking references to macros, and implementing such a
feature would be complicated.

Bug: v8:7793
Change-Id: I09120960e3492dd51be0d4c57e14ff3826b99262
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2701752
Commit-Queue: Seth Brenith <seth.brenith@microsoft.com>
Reviewed-by: Nico Hartmann <nicohartmann@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/master@{#72964}
This commit is contained in:
Seth Brenith 2021-02-23 07:58:06 -08:00 committed by Commit Bot
parent ef1ae61976
commit ede2740711
16 changed files with 296 additions and 7 deletions

View File

@ -112,6 +112,32 @@ type bool generates 'TNode<BoolT>' constexpr 'bool';
type bint generates 'TNode<BInt>' constexpr 'BInt';
type string constexpr 'const char*';
// Represents a std::function which produces the generated TNode type of T.
// Useful for passing values to and from CSA code that uses LazyNode<T>, which
// is a typedef for std::function<TNode<T>()>. Can be created with %MakeLazy and
// accessed with RunLazy.
type Lazy<T: type>;
// Makes a Lazy. The first parameter is the name of a macro, which is looked up
// in the context where %MakeLazy is called, as a workaround for the fact that
// macros can't be used as values directly. The other parameters are saved and
// passed to the macro when somebody runs the resulting Lazy object. Torque
// syntax doesn't allow for arbitrary-length generic macros, but the internals
// support any number of parameters, so if you need more parameters, feel free
// to add additional declarations here.
intrinsic %MakeLazy<T: type>(getter: constexpr string): Lazy<T>;
intrinsic %MakeLazy<T: type, A1: type>(
getter: constexpr string, arg1: A1): Lazy<T>;
intrinsic %MakeLazy<T: type, A1: type, A2: type>(
getter: constexpr string, arg1: A1, arg2: A2): Lazy<T>;
intrinsic %MakeLazy<T: type, A1: type, A2: type, A3: type>(
getter: constexpr string, arg1: A1, arg2: A2, arg3: A3): Lazy<T>;
// Executes a Lazy and returns the result. The CSA-side definition is a
// template, but Torque doesn't understand how to use templates for extern
// macros, so just add whatever overload definitions you need here.
extern macro RunLazy(Lazy<Smi>): Smi;
// A Smi value containing a bitfield struct as its integer data.
@useParentTypeChecker type SmiTagged<T : type extends uint31> extends Smi;

View File

@ -444,6 +444,11 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
return CAST(heap_object);
}
template <typename T>
TNode<T> RunLazy(LazyNode<T> lazy) {
return lazy();
}
#define PARAMETER_BINOP(OpName, IntPtrOpName, SmiOpName) \
TNode<Smi> OpName(TNode<Smi> a, TNode<Smi> b) { return SmiOpName(a, b); } \
TNode<IntPtrT> OpName(TNode<IntPtrT> a, TNode<IntPtrT> b) { \

View File

@ -261,6 +261,11 @@ void CCGenerator::EmitInstruction(
ReportError("Not supported in C++ output: CallCsaMacroAndBranch");
}
void CCGenerator::EmitInstruction(const MakeLazyNodeInstruction& instruction,
Stack<std::string>* stack) {
ReportError("Not supported in C++ output: MakeLazyNode");
}
void CCGenerator::EmitInstruction(const CallBuiltinInstruction& instruction,
Stack<std::string>* stack) {
ReportError("Not supported in C++ output: CallBuiltin");

View File

@ -73,6 +73,7 @@ static const char* const MUTABLE_SLICE_TYPE_STRING = "MutableSlice";
static const char* const CONST_SLICE_TYPE_STRING = "ConstSlice";
static const char* const WEAK_TYPE_STRING = "Weak";
static const char* const SMI_TAGGED_TYPE_STRING = "SmiTagged";
static const char* const LAZY_TYPE_STRING = "Lazy";
static const char* const UNINITIALIZED_ITERATOR_TYPE_STRING =
"UninitializedIterator";
static const char* const GENERIC_TYPE_INSTANTIATION_NAMESPACE_STRING =

View File

@ -479,6 +479,41 @@ void CSAGenerator::EmitInstruction(
}
}
void CSAGenerator::EmitInstruction(const MakeLazyNodeInstruction& instruction,
Stack<std::string>* stack) {
TypeVector parameter_types =
instruction.macro->signature().parameter_types.types;
std::vector<std::string> args = ProcessArgumentsCommon(
parameter_types, instruction.constexpr_arguments, stack);
std::string result_name =
DefinitionToVariable(instruction.GetValueDefinition());
stack->Push(result_name);
decls() << " " << instruction.result_type->GetGeneratedTypeName() << " "
<< result_name << ";\n";
// We assume here that the CodeAssemblerState will outlive any usage of
// the generated std::function that binds it. Likewise, copies of TNode values
// are only valid during generation of the current builtin.
out() << " " << result_name << " = [=] () { return ";
bool first = true;
if (const ExternMacro* extern_macro =
ExternMacro::DynamicCast(instruction.macro)) {
out() << extern_macro->external_assembler_name() << "(state_)."
<< extern_macro->ExternalName() << "(";
} else {
out() << instruction.macro->ExternalName() << "(state_";
first = false;
}
if (!args.empty()) {
if (!first) out() << ", ";
PrintCommaSeparatedList(out(), args);
}
out() << "); };\n";
}
void CSAGenerator::EmitInstruction(const CallBuiltinInstruction& instruction,
Stack<std::string>* stack) {
std::vector<std::string> arguments = stack->PopMany(instruction.argc);
@ -1012,7 +1047,7 @@ void CSAGenerator::EmitCSAValue(VisitResult result,
out << "}";
} else {
DCHECK_EQ(1, result.stack_range().Size());
out << "TNode<" << result.type()->GetGeneratedTNodeTypeName() << ">{"
out << result.type()->GetGeneratedTypeName() << "{"
<< values.Peek(result.stack_range().begin()) << "}";
}
}

View File

@ -18,6 +18,18 @@ namespace torque {
DEFINE_CONTEXTUAL_VARIABLE(CurrentScope)
QualifiedName QualifiedName::Parse(std::string qualified_name) {
std::vector<std::string> qualifications;
while (true) {
size_t namespace_delimiter_index = qualified_name.find("::");
if (namespace_delimiter_index == std::string::npos) break;
qualifications.push_back(
qualified_name.substr(0, namespace_delimiter_index));
qualified_name = qualified_name.substr(namespace_delimiter_index + 2);
}
return QualifiedName(qualifications, qualified_name);
}
std::ostream& operator<<(std::ostream& os, const QualifiedName& name) {
for (const std::string& qualifier : name.namespace_qualification) {
os << qualifier << "::";

View File

@ -36,6 +36,8 @@ struct QualifiedName {
explicit QualifiedName(std::string name)
: QualifiedName({}, std::move(name)) {}
static QualifiedName Parse(std::string qualified_name);
bool HasNamespaceQualification() const {
return !namespace_qualification.empty();
}

View File

@ -2935,6 +2935,68 @@ VisitResult ImplementationVisitor::GenerateCall(
const Field& field =
class_type->LookupField(StringLiteralUnquote(constexpr_arguments[0]));
return GenerateArrayLength(VisitResult(type, argument_range), field);
} else if (intrinsic->ExternalName() == "%MakeLazy") {
if (specialization_types[0]->IsStructType()) {
ReportError("%MakeLazy can't use macros that return structs");
}
std::string getter_name = StringLiteralUnquote(constexpr_arguments[0]);
// Normally the parser would split namespace names for us, but we
// sidestepped it by putting the macro name in a string literal.
QualifiedName qualified_getter_name = QualifiedName::Parse(getter_name);
// converted_arguments contains all of the arguments to %MakeLazy. We're
// looking for a function that takes all but the first.
Arguments arguments_to_getter;
arguments_to_getter.parameters.insert(
arguments_to_getter.parameters.begin(),
converted_arguments.begin() + 1, converted_arguments.end());
Callable* callable = LookupCallable(
qualified_getter_name, Declarations::Lookup(qualified_getter_name),
arguments_to_getter, {});
Macro* getter = Macro::DynamicCast(callable);
if (!getter || getter->IsMethod()) {
ReportError(
"%MakeLazy expects a macro, not builtin or other type of callable");
}
if (!getter->signature().labels.empty()) {
ReportError("%MakeLazy requires a macro with no labels");
}
if (!getter->signature().return_type->IsSubtypeOf(
specialization_types[0])) {
ReportError("%MakeLazy expected return type ", *specialization_types[0],
" but found ", *getter->signature().return_type);
}
if (getter->signature().implicit_count > 0) {
ReportError("Implicit parameters are not yet supported in %MakeLazy");
}
getter->SetUsed(); // Prevent warnings about unused macros.
// Now that we've looked up the getter macro, we have to convert the
// arguments again, so that, for example, constexpr arguments can be
// coerced to non-constexpr types and put on the stack.
std::vector<VisitResult> converted_arguments_for_getter;
StackRange argument_range_for_getter = assembler().TopRange(0);
std::vector<std::string> constexpr_arguments_for_getter;
size_t current = 0;
for (auto arg : arguments_to_getter.parameters) {
DCHECK_LT(current, getter->signature().types().size());
const Type* to_type = getter->signature().types()[current++];
AddCallParameter(getter, arg, to_type, &converted_arguments_for_getter,
&argument_range_for_getter,
&constexpr_arguments_for_getter,
/*inline_macro=*/false);
}
// Now that the arguments are prepared, emit the instruction that consumes
// them.
assembler().Emit(MakeLazyNodeInstruction{getter, return_type,
constexpr_arguments_for_getter});
return VisitResult(return_type, assembler().TopRange(1));
} else {
assembler().Emit(CallIntrinsicInstruction{intrinsic, specialization_types,
constexpr_arguments});

View File

@ -663,6 +663,36 @@ DefinitionLocation StoreBitFieldInstruction::GetValueDefinition() const {
return DefinitionLocation::Instruction(this, 0);
}
void MakeLazyNodeInstruction::TypeInstruction(Stack<const Type*>* stack,
ControlFlowGraph* cfg) const {
std::vector<const Type*> parameter_types =
LowerParameterTypes(macro->signature().parameter_types);
for (intptr_t i = parameter_types.size() - 1; i >= 0; --i) {
const Type* arg_type = stack->Pop();
const Type* parameter_type = parameter_types.back();
parameter_types.pop_back();
if (arg_type != parameter_type) {
ReportError("parameter ", i, ": expected type ", *parameter_type,
" but found type ", *arg_type);
}
}
stack->Push(result_type);
}
void MakeLazyNodeInstruction::RecomputeDefinitionLocations(
Stack<DefinitionLocation>* locations, Worklist<Block*>* worklist) const {
auto parameter_types =
LowerParameterTypes(macro->signature().parameter_types);
locations->PopMany(parameter_types.size());
locations->Push(GetValueDefinition());
}
DefinitionLocation MakeLazyNodeInstruction::GetValueDefinition() const {
return DefinitionLocation::Instruction(this, 0);
}
bool CallRuntimeInstruction::IsBlockTerminator() const {
return is_tailcall || runtime_function->signature().return_type ==
TypeOracle::GetNeverType();

View File

@ -49,6 +49,7 @@ class RuntimeFunction;
V(ConstexprBranchInstruction) \
V(GotoInstruction) \
V(GotoExternalInstruction) \
V(MakeLazyNodeInstruction) \
V(ReturnInstruction) \
V(PrintConstantStringInstruction) \
V(AbortInstruction) \
@ -457,6 +458,21 @@ struct CallCsaMacroAndBranchInstruction : InstructionBase {
base::Optional<Block*> catch_block;
};
struct MakeLazyNodeInstruction : InstructionBase {
TORQUE_INSTRUCTION_BOILERPLATE()
MakeLazyNodeInstruction(Macro* macro, const Type* result_type,
std::vector<std::string> constexpr_arguments)
: macro(macro),
result_type(result_type),
constexpr_arguments(std::move(constexpr_arguments)) {}
DefinitionLocation GetValueDefinition() const;
Macro* macro;
const Type* result_type;
std::vector<std::string> constexpr_arguments;
};
struct CallBuiltinInstruction : InstructionBase {
TORQUE_INSTRUCTION_BOILERPLATE()
bool IsBlockTerminator() const override { return is_tailcall; }

View File

@ -114,6 +114,10 @@ class TypeOracle : public ContextualClass<TypeOracle> {
return Declarations::LookupGlobalUniqueGenericType(SMI_TAGGED_TYPE_STRING);
}
static GenericType* GetLazyGeneric() {
return Declarations::LookupGlobalUniqueGenericType(LAZY_TYPE_STRING);
}
static const Type* GetReferenceType(const Type* referenced_type,
bool is_const) {
return GetGenericTypeInstance(GetReferenceGeneric(is_const),

View File

@ -184,6 +184,23 @@ std::string Type::GetGeneratedTNodeTypeName() const {
return result;
}
std::string AbstractType::GetGeneratedTypeNameImpl() const {
// A special case that is not very well represented by the "generates"
// syntax in the .tq files: Lazy<T> represents a std::function that
// produces a TNode of the wrapped type.
if (base::Optional<const Type*> type_wrapped_in_lazy =
Type::MatchUnaryGeneric(this, TypeOracle::GetLazyGeneric())) {
DCHECK(!IsConstexpr());
return "std::function<" + (*type_wrapped_in_lazy)->GetGeneratedTypeName() +
"()>";
}
if (generated_type_.empty()) {
return parent()->GetGeneratedTypeName();
}
return IsConstexpr() ? generated_type_ : "TNode<" + generated_type_ + ">";
}
std::string AbstractType::GetGeneratedTNodeTypeNameImpl() const {
if (generated_type_.empty()) return parent()->GetGeneratedTNodeTypeName();
return generated_type_;

View File

@ -268,12 +268,7 @@ class AbstractType final : public Type {
DECLARE_TYPE_BOILERPLATE(AbstractType)
const std::string& name() const { return name_; }
std::string ToExplicitString() const override { return name(); }
std::string GetGeneratedTypeNameImpl() const override {
if (generated_type_.empty()) {
return parent()->GetGeneratedTypeName();
}
return IsConstexpr() ? generated_type_ : "TNode<" + generated_type_ + ">";
}
std::string GetGeneratedTypeNameImpl() const override;
std::string GetGeneratedTNodeTypeNameImpl() const override;
bool IsConstexpr() const final {
const bool is_constexpr = flags_ & AbstractTypeFlag::kConstexpr;

View File

@ -898,6 +898,41 @@ TEST(TestCallMultiReturnBuiltin) {
ft.Call();
}
TEST(TestRunLazyTwice) {
CcTest::InitializeVM();
Isolate* isolate(CcTest::i_isolate());
i::HandleScope scope(isolate);
const int kNumParams = 0;
int lazyNumber = 3;
CodeAssemblerTester asm_tester(isolate, kNumParams);
TestTorqueAssembler m(asm_tester.state());
{
CodeStubAssembler::LazyNode<Smi> lazy = [&]() {
return m.SmiConstant(lazyNumber++);
};
m.Return(m.TestRunLazyTwice(lazy));
}
CHECK_EQ(lazyNumber, 5);
FunctionTester ft(asm_tester.GenerateCode(), kNumParams);
Handle<Object> result = ft.Call().ToHandleChecked();
CHECK_EQ(7, Handle<Smi>::cast(result)->value());
}
TEST(TestCreateLazyNodeFromTorque) {
CcTest::InitializeVM();
Isolate* isolate(CcTest::i_isolate());
i::HandleScope scope(isolate);
const int kNumParams = 0;
CodeAssemblerTester asm_tester(isolate, kNumParams);
TestTorqueAssembler m(asm_tester.state());
{
m.TestCreateLazyNodeFromTorque();
m.Return(m.UndefinedConstant());
}
FunctionTester ft(asm_tester.GenerateCode(), kNumParams);
ft.Call();
}
} // namespace compiler
} // namespace internal
} // namespace v8

View File

@ -1367,4 +1367,47 @@ macro TestCallMultiReturnBuiltin(implicit context: Context)() {
check(result.a == 445);
check(result.b == FromConstexpr<String>('hi').map);
}
@export
macro TestRunLazyTwice(lazySmi: Lazy<Smi>): Smi {
const firstResult = RunLazy(lazySmi);
const secondResult = RunLazy(lazySmi);
return firstResult + secondResult;
}
macro GetLazySmi(): Smi {
return 3;
}
macro AddTwoSmiValues(a: Smi, b: Smi): Smi {
return a + b;
}
macro AddSmiAndConstexprValues(a: Smi, b: constexpr int31): Smi {
return a + b;
}
@export
macro TestCreateLazyNodeFromTorque() {
const lazy = %MakeLazy<Smi>('GetLazySmi');
const result = TestRunLazyTwice(lazy);
check(result == 6);
// The macro can also be referred to using namespace qualifications.
const lazy2 = %MakeLazy<Smi>('test::GetLazySmi');
const result2 = TestRunLazyTwice(lazy2);
check(result2 == 6);
// We can save params to the macro. The most common usage is likely a
// single-arg macro that just returns the arg, but we can use any number of
// params.
const lazy3 = %MakeLazy<Smi>('AddTwoSmiValues', 5, 6);
const result3 = TestRunLazyTwice(lazy3);
check(result3 == 22);
// It's okay if some of the params are constexpr and some aren't.
const lazy4 = %MakeLazy<Smi>('AddSmiAndConstexprValues', 7, 8);
const result4 = TestRunLazyTwice(lazy4);
check(result4 == 30);
}
}

View File

@ -89,6 +89,7 @@ type SmiTagged<T : type extends uint31> extends Smi;
type String extends HeapObject;
type HeapNumber extends HeapObject;
type FixedArrayBase extends HeapObject;
type Lazy<T: type>;
struct float64_or_hole {
is_hole: bool;