diff --git a/toolchain/benchmarking/compile_benchmark.cpp b/toolchain/benchmarking/compile_benchmark.cpp index bea5f8698f74..10d072ec3639 100644 --- a/toolchain/benchmarking/compile_benchmark.cpp +++ b/toolchain/benchmarking/compile_benchmark.cpp @@ -262,6 +262,7 @@ static auto DenseDeclParams() -> SourceGen::DenseDeclParams { params.class_params.inline_function_defs = 1; params.class_params.inline_getters = 1; params.class_params.inline_forwarders = 1; + params.free_function_decls_per_class = 2; return params; } @@ -283,6 +284,7 @@ static auto ImplFileParams() -> SourceGen::DenseDeclParams { .private_method_decl_params = {.max_params = 2}, .private_field_decls = 4}; params.define_decls_out_of_line = true; + params.free_function_decls_per_class = 2; return params; } @@ -302,6 +304,7 @@ static auto RealisticHeaderParams() -> SourceGen::DenseDeclParams { .inline_getters = 2, .inline_predicates = 1, .inline_forwarders = 2}; + params.free_function_decls_per_class = 1; return params; } diff --git a/toolchain/benchmarking/source_gen.cpp b/toolchain/benchmarking/source_gen.cpp index b97361736be2..a04b93a4fcb2 100644 --- a/toolchain/benchmarking/source_gen.cpp +++ b/toolchain/benchmarking/source_gen.cpp @@ -38,11 +38,11 @@ static constexpr int MinMemberNameLength = 4; // The length of every local variable name. How many times a definition spells // each local depends on the local's position, so a single length keeps the byte // count from depending on which name lands where. Locals are shorter than class -// names, so they can't shadow a class that the definition names. This is -// hard-coded rather than in `ClassParams` because seed-independent output sizes -// require a single length `< MinClassNameLength`; it would only need to be -// parameterized to benchmark identifier-length sensitivity specifically for -// local variables. +// and free function names, so they can't shadow a class or free function that +// the definition names. This is hard-coded rather than in `ClassParams` because +// seed-independent output sizes require a single length `< MinClassNameLength`; +// it would only need to be parameterized to benchmark identifier-length +// sensitivity specifically for local variables. static constexpr int LocalNameLength = 3; // The number of function and method declarations in each class. @@ -85,11 +85,11 @@ static auto WeightedFixedTypes( // the arrangement doesn't change the definition's size. // // TODO: Plans read each parameter once, read class-typed locals only with -// `Checksum`, and make no calls. Real definitions read parameters and fields -// throughout, and make calls in most statements, many of them to methods of -// their locals. Add those statements and expressions, fixing each one's -// spelling before the shuffle as the type use pools do, so that sizes stay the -// same for every seed. +// `Checksum`, and only call free functions, with fixed arguments. Real +// definitions read parameters and fields throughout, and call methods of their +// locals with arguments computed from other locals. Add those statements and +// expressions, fixing each one's spelling before the shuffle as the type use +// pools do, so that sizes stay the same for every seed. enum class PlanStepKind : uint8_t { // `var : i32 = ;`, whose local is available to be read and // updated by subsequent steps. @@ -104,8 +104,19 @@ enum class PlanStepKind : uint8_t { // (and updated, when `var`) by subsequent steps. `spelling_index` selects // among `DestructureForms`. Destructure, + // A declaration that destructures the tuple returned by a call to a free + // function into two locals, such as + // `let (: bool, : i64) = ;`, whose locals are read by the + // next step and whose `i32` locals are available to be read (but not updated) + // by subsequent steps. When no free function returns a tuple in + // `CallDestructureForms`, this falls back to `Destructure` using + // `spelling_index`. + DestructureCall, // ` = ;` Update, + // ` = + ;`, where a consumer template reads the result + // of a call to a free function. + Call, // `if () {`, a block, and `}`. If, // `if () {`, a block, `} else {`, a block, and `}`. @@ -145,7 +156,8 @@ static auto NumPlanBlocks(PlanStepKind kind) -> int { static auto IsLocalDeclaration(PlanStepKind kind) -> bool { return kind == PlanStepKind::ScalarLocal || kind == PlanStepKind::ClassTypeLocal || - kind == PlanStepKind::Destructure; + kind == PlanStepKind::Destructure || + kind == PlanStepKind::DestructureCall; } // A step in a definition plan. `spelling_index` selects among the available @@ -201,6 +213,36 @@ static constexpr DestructureForm DestructureForms[] = { .b_is_i32 = true}, }; +// A form of `DestructureCall` step, for the tuple type that its callee returns. +// `{a}` and `{b}` are the two locals, and `read` reads them along with an +// operand, `{0}`, since the call has none. Later steps can also read the locals +// that are `i32`s. C++ destructures every call with `const auto [{a}, {b}]`. +struct CallDestructureForm { + llvm::StringLiteral carbon_type; + llvm::StringLiteral cpp_type; + llvm::StringLiteral carbon_pattern; + llvm::StringLiteral carbon_read; + llvm::StringLiteral cpp_read; + bool a_is_i32; + bool b_is_i32; +}; +static constexpr CallDestructureForm CallDestructureForms[] = { + {.carbon_type = "(bool, i64)", + .cpp_type = "std::pair", + .carbon_pattern = "let ({a}: bool, {b}: i64)", + .carbon_read = "({0} + (if {a} then 1 else 0) + ({b} as i32))", + .cpp_read = "({0} + ({a} ? 1 : 0) + static_cast({b}))", + .a_is_i32 = false, + .b_is_i32 = false}, + {.carbon_type = "(i32, i64*)", + .cpp_type = "std::pair", + .carbon_pattern = "let ({a}: i32, {b}: i64*)", + .carbon_read = "({0} + {a} + (*{b} as i32))", + .cpp_read = "({0} + {a} + static_cast(*{b}))", + .a_is_i32 = true, + .b_is_i32 = false}, +}; + // The number of distinct spellings for a step of `kind`. static auto NumPlanSpellings(PlanStepKind kind) -> int { switch (kind) { @@ -208,8 +250,12 @@ static auto NumPlanSpellings(PlanStepKind kind) -> int { case PlanStepKind::Update: return std::size(PlanExprs); case PlanStepKind::ClassTypeLocal: + case PlanStepKind::Call: return 1; case PlanStepKind::Destructure: + case PlanStepKind::DestructureCall: + // `DestructureCall` uses `DestructureForms` when no free function returns + // a destructurable tuple. return std::size(DestructureForms); case PlanStepKind::If: case PlanStepKind::IfElse: @@ -229,7 +275,9 @@ static auto NumPlanSpellings(PlanStepKind kind) -> int { // returns or exits with is one depth below the guard, and isn't listed. struct DefPlan { llvm::SmallVector> levels; - // The number of lines in the plan's steps. + // The number of lines in the plan's steps. A definition whose plan has calls + // or destructured calls also has a line declaring `acc64`, which `lines` + // counts. int lines = 0; // The number of local variables that the plan's steps declare. int locals = 0; @@ -237,6 +285,10 @@ struct DefPlan { int class_type_locals = 0; // The class names of the plan's class-typed locals, with `""` for `Self`. llvm::SmallVector class_type_local_types; + // The number of `Call` steps in the plan. + int calls = 0; + // The number of `DestructureCall` steps in the plan. + int destructure_calls = 0; }; // The internal shape calibration of definition plans, which approximates the @@ -251,7 +303,7 @@ struct DefPlan { // // All rates and bucket distributions below are integer percentages (with // multi-bucket rows summing to 100). -// + // A row of `PlanShapeTable` giving the shape parameters for plans with at // least `min_statements` statements: the percentage of plans with each maximum // depth, and the number of guard returns at a depth as a percentage of the @@ -317,14 +369,21 @@ static constexpr int ElseIfPercent = 42; // How many times more likely a loop block is than another block of the same // size to get each further statement when arranging a definition. static constexpr int LoopBlockWeight = 3; +// The percentage of updates that are calls, when definitions can make calls. +static constexpr int CallPercent = 50; +// The percentage of destructurings that destructure a call, when definitions +// can make calls. In the sampled Carbon, LLVM, and Clang implementation files, +// 89% of structured bindings bind a call's result. +static constexpr int CallDestructurePercent = 89; // The percentages of the statements at depth 0, and deeper, that declare a new // local variable. static constexpr int TopLocalPercent = 35; static constexpr int NestedLocalPercent = 25; // The percentages of declarations that destructure a tuple, and of the rest // that declare a local of a class type. Structured bindings are 0.2-1.9% of the -// declarations in the sample, and Carbon's tuples suggest the high end. Class -// types are 29-78% of the types of local variables, 49% on average. +// declarations in the sampled Carbon, LLVM, and Clang implementation files, and +// Carbon's tuples suggest the high end. Class types are 29-78% of the types of +// local variables, 49% on average. static constexpr int DestructurePercent = 2; static constexpr int ClassTypeLocalPercent = 50; @@ -378,11 +437,12 @@ static auto SplitPlanDepths(int statements, int max_depth, } // Composes the plan of a definition with `statements` statements, for the -// `variant`th definition of that size. Percentage-scaled counts round with a +// `variant`th definition of that size. With `calls`, some of the updates and +// destructurings call free functions. Percentage-scaled counts round with a // generator seeded with the size and variant, so that definitions of one size // vary while their average follows the calibration, and nothing depends on the // `SourceGen` seed. -static auto ComposeDefPlan(int statements, int variant) -> DefPlan { +static auto ComposeDefPlan(int statements, int variant, bool calls) -> DefPlan { DefPlan plan; if (statements == 0) { return plan; @@ -504,11 +564,16 @@ static auto ComposeDefPlan(int statements, int variant) -> DefPlan { int destructures = scale_by_percent(locals, DestructurePercent); int class_type_locals = scale_by_percent(locals - destructures, ClassTypeLocalPercent); + int destructure_calls = + calls ? scale_by_percent(destructures, CallDestructurePercent) : 0; + int num_calls = calls ? scale_by_percent(updates, CallPercent) : 0; add(depth, PlanStepKind::ScalarLocal, locals - destructures - class_type_locals); add(depth, PlanStepKind::ClassTypeLocal, class_type_locals); - add(depth, PlanStepKind::Destructure, destructures); - add(depth, PlanStepKind::Update, updates); + add(depth, PlanStepKind::Destructure, destructures - destructure_calls); + add(depth, PlanStepKind::DestructureCall, destructure_calls); + add(depth, PlanStepKind::Update, updates - num_calls); + add(depth, PlanStepKind::Call, num_calls); add(depth, PlanStepKind::If, ifs); add(depth, PlanStepKind::IfElse, pairs - else_ifs); add(depth, PlanStepKind::ElseIf, else_ifs); @@ -522,10 +587,17 @@ static auto ComposeDefPlan(int statements, int variant) -> DefPlan { // Each destructuring declares two locals. plan.locals += locals + destructures; plan.class_type_locals += class_type_locals; + plan.calls += num_calls; + plan.destructure_calls += destructure_calls; guard_children = guards + exits; blocks = next_blocks; loop_blocks = loops; } + // Definitions with calls or destructured calls declare `acc64` on an extra + // line before `acc`; see `GenerateFunctionDef`. + if (plan.calls + plan.destructure_calls > 0) { + ++plan.lines; + } return plan; } @@ -563,12 +635,14 @@ static auto ApportionDefSizes(int num_defs, return counts; } -// Builds the plans for `num_defs` definitions, with sizes following `sizes`, -// and appends them to `plans`. Each size's plans spread evenly through the -// order, so that any stretch of the order has a mix of sizes. +// Builds the plans for `num_defs` definitions, with sizes following `sizes` and +// with calls when `calls`, and appends them to `plans`. Each size's plans +// spread evenly through the order, so that any stretch of the order has a mix +// of sizes. static auto BuildDefPlans(int num_defs, llvm::ArrayRef sizes, - llvm::SmallVectorImpl& plans) -> void { + bool calls, llvm::SmallVectorImpl& plans) + -> void { // Entries are sorted by `position`, which is `(variant + 0.5) / count`, to // interleave buckets with different counts. struct Entry { @@ -591,16 +665,17 @@ static auto BuildDefPlans(int num_defs, }); plans.reserve(plans.size() + num_defs); for (const Entry& entry : entries) { - plans.push_back(ComposeDefPlan(entry.statements, entry.variant)); + plans.push_back(ComposeDefPlan(entry.statements, entry.variant, calls)); } } // Estimates the average number of lines in the plans of definitions with sizes -// following `sizes`. Uses `double` (like the other `EstimateAvg*Lines` helpers -// below) to accumulate fractional average line counts across variants and -// constructs before a single final rounding to a whole number of classes. +// following `sizes`, and with calls when `calls`. Uses `double` (like the +// other `EstimateAvg*Lines` helpers below) to accumulate fractional average +// line counts across variants and constructs before a single final rounding to +// a whole number of classes. static auto EstimateAvgDefPlanLines( - llvm::ArrayRef sizes) -> double { + llvm::ArrayRef sizes, bool calls) -> double { // Average over enough variants that their rounding evens out. constexpr int NumVariants = 16; double lines = 0.0; @@ -608,7 +683,7 @@ static auto EstimateAvgDefPlanLines( for (const SourceGen::DefSizeWeight& size : sizes) { int size_lines = 0; for (int variant : llvm::seq(NumVariants)) { - size_lines += ComposeDefPlan(size.statements, variant).lines; + size_lines += ComposeDefPlan(size.statements, variant, calls).lines; } lines += static_cast(size_lines) / NumVariants * size.weight; total_weight += size.weight; @@ -626,6 +701,29 @@ static auto EstimateAvgDefPlanLines( // Detailed comments for out-of-line methods are on their definitions. class SourceGen::ClassGenState { public: + // A free function declaration, which definitions call. `args` is the argument + // list of every call to it. + struct FreeFunction { + llvm::StringRef name; + llvm::SmallVector param_types; + std::string args; + llvm::StringRef return_type; + }; + + // A call in a definition: the index of the callee in `free_functions()`, and + // the consumer template that reads its result. + struct CallRef { + int callee_index; + llvm::StringRef consumer; + }; + + // A destructured call in a definition: the index of the callee in + // `free_functions()`, and the index of its form in `CallDestructureForms`. + struct DestructureCallRef { + int callee_index; + int form_index; + }; + ClassGenState(SourceGen& gen, int num_classes, const DenseDeclParams& params); auto public_function_param_counts() -> llvm::SmallVectorImpl& { @@ -654,6 +752,45 @@ class SourceGen::ClassGenState { auto def_plans() -> llvm::ArrayRef { return def_plans_; } auto inline_def_plans_start() -> int { return inline_def_plans_start_; } + auto free_functions() -> llvm::ArrayRef { + return free_functions_; + } + // The shuffled order to declare the free functions in. + auto free_emission_order() -> llvm::ArrayRef { + return free_emission_order_; + } + auto free_param_names() -> llvm::SmallVectorImpl& { + return free_param_names_; + } + + // The calls that definition plans make, shuffled. + auto call_refs() -> llvm::SmallVectorImpl& { return call_refs_; } + // The destructured calls that definition plans make, shuffled. + auto destructure_call_refs() -> llvm::SmallVectorImpl& { + return destructure_call_refs_; + } + + // Emits the next call in `call_refs()`, read with its consumer template. + auto EmitCall(llvm::raw_ostream& os) -> void { + CallRef ref = call_refs_.pop_back_val(); + const FreeFunction& callee = free_functions_[ref.callee_index]; + auto [prefix, suffix] = ref.consumer.split("{0}"); + os << prefix << callee.name << "(" << callee.args << ")" << suffix; + } + + // Takes the next destructured call, spells it in `call`, and returns the form + // that destructures it. Returns null when no free function returns a tuple + // with a form. + auto NextDestructureCall(std::string& call) -> const CallDestructureForm* { + if (destructure_call_refs_.empty()) { + return nullptr; + } + DestructureCallRef ref = destructure_call_refs_.pop_back_val(); + const FreeFunction& callee = free_functions_[ref.callee_index]; + call = (callee.name + "(" + callee.args + ")").str(); + return &CallDestructureForms[ref.form_index]; + } + auto getter_field_types() -> llvm::SmallVectorImpl& { return getter_field_types_; } @@ -704,10 +841,11 @@ class SourceGen::ClassGenState { valid_type_names_.Insert(type_name); } - // The names of all classes in the file. Parameter, member, and field names - // exclude these so that they can't shadow a class that a definition names. - auto class_name_set() -> const Set& { - return class_name_set_; + // The names of all classes and free functions in the file. Parameter, + // member, and field names exclude these so that they can't shadow a name that + // a definition uses. + auto file_scope_name_set() -> const Set& { + return file_scope_name_set_; } // Emits an expression producing a value of `type`: a call to the class's @@ -793,8 +931,15 @@ class SourceGen::ClassGenState { auto GetValidReturnType(TypePool& pool, bool is_inline) -> TypeUse; auto BuildClassAndTypeNames(SourceGen& gen, int num_classes, - const ClassParams& class_params, - const TypeUseParams& type_use_params) -> void; + const DenseDeclParams& params) -> void; + auto BuildFreeFunction( + SourceGen& gen, int i, llvm::StringRef name, + llvm::ArrayRef weighted_types, + int max_params) -> FreeFunction; + auto BuildCallRefs(SourceGen& gen, int num_calls) + -> llvm::SmallVector; + auto BuildDestructureCallRefs(SourceGen& gen, int num_calls) + -> llvm::SmallVector; auto BuildTypePool(SourceGen& gen, int num_types, int max_refs_per_class, bool needs_value_expr, bool consumed, bool exclude_last_class, @@ -811,6 +956,11 @@ class SourceGen::ClassGenState { llvm::SmallVector forwarder_param_counts_; llvm::SmallVector def_plans_; int inline_def_plans_start_; + llvm::SmallVector free_functions_; + llvm::SmallVector free_emission_order_; + llvm::SmallVector free_param_names_; + llvm::SmallVector call_refs_; + llvm::SmallVector destructure_call_refs_; // Getters return a copy of their field, and `Make` initializes it, so getter // and predicate fields have fixed types with a value expression. These types @@ -856,6 +1006,7 @@ class SourceGen::ClassGenState { Set valid_type_names_; Set class_name_set_; + Set file_scope_name_set_; // The value expression and consumer templates of each fixed type, keyed by // its spelling. These refer into the `TypeUseParams`, which outlives // generation. @@ -921,10 +1072,13 @@ SourceGen::ClassGenState::ClassGenState(SourceGen& gen, int num_classes, // `BuildClassAndTypeNames`. int num_inline_params = Sum(inline_function_param_counts_) + num_inline_functions; - BuildDefPlans(num_out_of_line_defs, params.out_of_line_def_sizes, def_plans_); + // Definitions only make calls when the file has free functions to call. + bool calls = params.free_function_decls_per_class > 0; + BuildDefPlans(num_out_of_line_defs, params.out_of_line_def_sizes, calls, + def_plans_); inline_def_plans_start_ = def_plans_.size(); BuildDefPlans(num_inline_functions, class_params.inline_function_def_sizes, - def_plans_); + calls, def_plans_); int num_locals = 0; for (const DefPlan& plan : def_plans_) { num_locals += plan.locals; @@ -950,8 +1104,7 @@ SourceGen::ClassGenState::ClassGenState(SourceGen& gen, int num_classes, forwarder_param_names_ = gen.GetShuffledIdentifiers(Sum(forwarder_param_counts_)); - BuildClassAndTypeNames(gen, num_classes, class_params, - params.type_use_params); + BuildClassAndTypeNames(gen, num_classes, params); } auto SourceGen::ClassGenState::GetValidTypeUse( @@ -1211,15 +1364,28 @@ auto SourceGen::ClassGenState::AssignClassTypeLocalTypes( // to each class at the number of uses each class draws from it after its // fields; see `BuildTypePool`. auto SourceGen::ClassGenState::BuildClassAndTypeNames( - SourceGen& gen, int num_classes, const ClassParams& class_params, - const TypeUseParams& type_use_params) -> void { - // Initially get the sequence of class names without shuffling so we can - // compute our type pools from them prior to any shuffling. + SourceGen& gen, int num_classes, const DenseDeclParams& params) -> void { + const ClassParams& class_params = params.class_params; + const TypeUseParams& type_use_params = params.type_use_params; + // Initially get the sequence of class and free function names without + // shuffling so we can compute our type pools from them prior to any + // shuffling. Drawing both from one set of unique names keeps them distinct, + // and `MinClassNameLength` keeps them longer than local and inline function + // names so those can't shadow them. + int num_free_functions = num_classes * params.free_function_decls_per_class; + llvm::SmallVector file_scope_names = + gen.GetUniqueIdentifiers(num_classes + num_free_functions, + /*min_length=*/MinClassNameLength); class_names_ = - gen.GetUniqueIdentifiers(num_classes, /*min_length=*/MinClassNameLength); + llvm::to_vector(llvm::ArrayRef(file_scope_names).take_front(num_classes)); + llvm::ArrayRef free_function_names = + llvm::ArrayRef(file_scope_names).slice(num_classes); for (llvm::StringRef name : class_names_) { class_name_set_.Insert(name); } + for (llvm::StringRef name : file_scope_names) { + file_scope_name_set_.Insert(name); + } // Every fixed type needs a consumer template, since any of them can be the // type of a consumed parameter. @@ -1358,12 +1524,143 @@ auto SourceGen::ClassGenState::BuildClassAndTypeNames( } std::shuffle(predicate_fields_.begin(), predicate_fields_.end(), gen.rng_); + free_functions_.reserve(num_free_functions); + auto free_function_types = WeightedFixedTypes( + type_use_params, [](const auto& /*fw*/) { return true; }); + CARBON_CHECK(num_free_functions == 0 || !free_function_types.empty(), + "Free functions need a fixed type."); + int num_free_params = 0; + for (auto [i, name] : llvm::enumerate(free_function_names)) { + free_functions_.push_back( + BuildFreeFunction(gen, static_cast(i), name, free_function_types, + params.free_function_decl_params.max_params)); + num_free_params += free_functions_.back().param_types.size(); + } + free_param_names_ = gen.GetShuffledIdentifiers(num_free_params); + int num_calls = 0; + int num_destructure_calls = 0; + for (const DefPlan& plan : def_plans_) { + num_calls += plan.calls; + num_destructure_calls += plan.destructure_calls; + } + call_refs_ = BuildCallRefs(gen, num_calls); + destructure_call_refs_ = BuildDestructureCallRefs(gen, num_destructure_calls); + free_emission_order_ = llvm::to_vector(llvm::seq(num_free_functions)); + std::shuffle(free_emission_order_.begin(), free_emission_order_.end(), + gen.rng_); // Classes are defined from the back, so the first unshuffled name is always // the class defined last, which field and return pools leave out; see // `BuildTypePool`. std::shuffle(class_names_.begin() + 1, class_names_.end(), gen.rng_); } +// Builds the `i`th free function declaration. Its signature depends only on +// `i`, not on the seed, so that the bytes of its calls don't either: its +// parameter count cycles through [0, max_params], its parameter types cycle +// through `weighted_types`, and its return type is the `i`th of +// `weighted_types`, so that return types follow the weights. +auto SourceGen::ClassGenState::BuildFreeFunction( + SourceGen& gen, int i, llvm::StringRef name, + llvm::ArrayRef weighted_types, + int max_params) -> FreeFunction { + FreeFunction fn; + fn.name = name; + + // Start each function's parameter types at a different point in the cycle. + int offset = i * (max_params + 2); + auto next_type = [&]() -> const TypeUseParams::FixedTypeWeight* { + return weighted_types[offset++ % weighted_types.size()]; + }; + + int param_count = i % (max_params + 1); + RawStringOstream args; + llvm::ListSeparator sep; + for (auto _ : llvm::seq(param_count)) { + const auto& fw = *next_type(); + fn.param_types.push_back(gen.IsCpp() ? fw.cpp_spelling + : fw.carbon_spelling); + llvm::StringRef arg = gen.IsCpp() ? fw.cpp_arg : fw.carbon_arg; + if (arg.empty()) { + arg = gen.IsCpp() ? fw.cpp_value : fw.carbon_value; + } + CARBON_CHECK(!arg.empty(), + "Fixed type `{0}` needs an argument or value expression.", + fn.param_types.back()); + args << sep << arg; + } + fn.args = args.TakeStr(); + const auto& return_type = *weighted_types[i % weighted_types.size()]; + fn.return_type = + gen.IsCpp() ? return_type.cpp_spelling : return_type.carbon_spelling; + return fn; +} + +// Builds a shuffled pool of `num_calls` calls, distributed evenly over the free +// functions. Each call gets one of its result type's consumer templates, +// round-robin per type before the shuffle. +auto SourceGen::ClassGenState::BuildCallRefs(SourceGen& gen, int num_calls) + -> llvm::SmallVector { + llvm::SmallVector refs; + if (num_calls == 0) { + return refs; + } + int num_free = free_functions_.size(); + CARBON_CHECK(num_free > 0); + refs.reserve(num_calls); + Map consumer_counters; + auto append_ref = [&](int callee_index) { + llvm::StringRef ret = free_functions_[callee_index].return_type; + llvm::ArrayRef consumers = + fixed_consumers_.Lookup(ret).value(); + int& counter = consumer_counters.Insert(ret, 0).value(); + refs.push_back({.callee_index = callee_index, + .consumer = consumers[counter++ % consumers.size()]}); + }; + int full_copies = num_calls / num_free; + int remainder = num_calls % num_free; + for (auto _ : llvm::seq(full_copies)) { + for (int callee_index : llvm::seq(num_free)) { + append_ref(callee_index); + } + } + // Give the remaining calls to a prefix of the unshuffled free functions, so + // that which functions get an extra call doesn't depend on the seed. + for (int callee_index : llvm::seq(remainder)) { + append_ref(callee_index); + } + std::shuffle(refs.begin(), refs.end(), gen.rng_); + return refs; +} + +// Builds a shuffled pool of `num_calls` destructured calls, distributed evenly +// over the free functions that return a tuple with a `CallDestructureForms` +// entry, with any remainder going to a fixed prefix of them. Returns an empty +// pool when there are none. +auto SourceGen::ClassGenState::BuildDestructureCallRefs(SourceGen& gen, + int num_calls) + -> llvm::SmallVector { + llvm::SmallVector eligible; + for (auto [callee_index, fn] : llvm::enumerate(free_functions_)) { + for (auto [form_index, call_form] : llvm::enumerate(CallDestructureForms)) { + if (fn.return_type == + (gen.IsCpp() ? call_form.cpp_type : call_form.carbon_type)) { + eligible.push_back({.callee_index = static_cast(callee_index), + .form_index = static_cast(form_index)}); + } + } + } + llvm::SmallVector refs; + if (eligible.empty()) { + return refs; + } + refs.reserve(num_calls); + for (int i : llvm::seq(num_calls)) { + refs.push_back(eligible[i % eligible.size()]); + } + std::shuffle(refs.begin(), refs.end(), gen.rng_); + return refs; +} + // Some heuristic numbers used when formatting generated code. These heuristics // are loosely based on what we expect to make Carbon code readable, and might // not fit as well in C++, but we use the same heuristics across languages for @@ -1410,15 +1707,22 @@ static auto EstimateAvgMethodDeclLines(SourceGen::MethodDeclParams params, // excluding its comment. The definition has an accumulator line, a line reading // the parameters, its plan's lines, a return line, and a closing brace line. static auto EstimateAvgInlineFunctionDefLines( - const SourceGen::ClassParams& params) -> double { + const SourceGen::DenseDeclParams& dense_params) -> double { + const SourceGen::ClassParams& params = dense_params.class_params; constexpr int ExtraParams = 1; return EstimateAvgFunctionDeclLines(params.inline_function_decl_params, ExtraParams) + - 4.0 + EstimateAvgDefPlanLines(params.inline_function_def_sizes); + 4.0 + + EstimateAvgDefPlanLines( + params.inline_function_def_sizes, + /*calls=*/dense_params.free_function_decls_per_class > 0); } // Note that this should match the heuristics used when formatting. // TODO: See top-level TODO about line estimates and formatting. +// +// This includes each class's share of the free function declarations, whose +// number is proportional to the number of classes. static auto EstimateAvgClassDefLines( const SourceGen::DenseDeclParams& dense_params) -> double { const SourceGen::ClassParams& params = dense_params.class_params; @@ -1426,6 +1730,12 @@ static auto EstimateAvgClassDefLines( // Comment line, and class open line. double avg = 2.0; + // Each class's share of the free function declarations, which each have a + // blank line and a comment line. + avg += dense_params.free_function_decls_per_class * + (2.0 + + EstimateAvgFunctionDeclLines(dense_params.free_function_decl_params)); + // Declarations with out-of-line definitions have an extra parameter. int decl_extra_params = define_decls_out_of_line ? 1 : 0; @@ -1442,7 +1752,7 @@ static auto EstimateAvgClassDefLines( avg += (2.0 + EstimateAvgMethodDeclLines(params.private_method_decl_params, decl_extra_params)) * params.private_method_decls; - avg += (2.0 + EstimateAvgInlineFunctionDefLines(params)) * + avg += (2.0 + EstimateAvgInlineFunctionDefLines(dense_params)) * params.inline_function_defs; // Getters and predicates are on one line. avg += 3.0 * (params.inline_getters + params.inline_predicates); @@ -1476,8 +1786,11 @@ static auto EstimateAvgClassDefLines( // line, an accumulator line, a line reading the parameters, its plan's lines, // a return line, and a closing brace line. if (define_decls_out_of_line) { - avg += (7.0 + EstimateAvgDefPlanLines(dense_params.out_of_line_def_sizes)) * - NumDeclsPerClass(params); + avg += + (7.0 + EstimateAvgDefPlanLines( + dense_params.out_of_line_def_sizes, + /*calls=*/dense_params.free_function_decls_per_class > 0)) * + NumDeclsPerClass(params); } // No need to account for the class close line, we have an extra blank line @@ -1529,6 +1842,7 @@ auto SourceGen::GenApiFileDenseDecls(int target_lines, } auto class_gen_state = ClassGenState(*this, num_classes, params); + GenerateFreeFunctionDecls(class_gen_state, source); llvm::SmallVector out_of_line_defs; for (auto _ : llvm::seq(num_classes)) { source << "\n"; @@ -1548,6 +1862,8 @@ auto SourceGen::GenApiFileDenseDecls(int target_lines, CARBON_CHECK(class_gen_state.private_function_param_counts().empty()); CARBON_CHECK(class_gen_state.private_method_param_counts().empty()); CARBON_CHECK(class_gen_state.inline_function_param_counts().empty()); + CARBON_CHECK(class_gen_state.call_refs().empty()); + CARBON_CHECK(class_gen_state.destructure_call_refs().empty()); CARBON_CHECK(class_gen_state.forwarder_param_counts().empty()); CARBON_CHECK(class_gen_state.getter_field_types().empty()); CARBON_CHECK(class_gen_state.predicate_fields().empty()); @@ -1558,6 +1874,7 @@ auto SourceGen::GenApiFileDenseDecls(int target_lines, CARBON_CHECK(class_gen_state.decl_names().empty()); CARBON_CHECK(class_gen_state.field_names().empty()); CARBON_CHECK(class_gen_state.param_names().empty()); + CARBON_CHECK(class_gen_state.free_param_names().empty()); CARBON_CHECK(class_gen_state.inline_function_names().empty()); CARBON_CHECK(class_gen_state.inline_param_names().empty()); CARBON_CHECK(class_gen_state.local_names().empty()); @@ -1677,8 +1994,8 @@ static constexpr llvm::StringRef NonCarbonCppKeywords[] = { // Names that generated code declares itself, which random identifiers must // avoid. For example, an inline function named `Make` would collide with its // class's `Make` function. -static constexpr llvm::StringRef ReservedGeneratedNames[] = {"Make", "Checksum", - "acc", "tag"}; +static constexpr llvm::StringRef ReservedGeneratedNames[] = { + "Make", "Checksum", "acc", "acc64", "tag"}; // Returns a random identifier string of the specified length. // @@ -2068,6 +2385,25 @@ auto SourceGen::EmitFunctionDecl(llvm::StringRef name, bool is_private, os << ") -> " << return_type << ";\n"; } +// Generates the file's free function declarations. +auto SourceGen::GenerateFreeFunctionDecls(ClassGenState& state, + llvm::raw_ostream& os) -> void { + for (int i : state.free_emission_order()) { + const ClassGenState::FreeFunction& fn = state.free_functions()[i]; + UniqueIdentifierPopper unique_param_names(*this, state.free_param_names(), + &state.file_scope_name_set()); + llvm::SmallVector params; + params.reserve(fn.param_types.size()); + for (llvm::StringRef type : fn.param_types) { + params.push_back({.name = unique_param_names.Pop(), .type = type}); + } + os << "\n// TODO: make better comment text\n"; + os << (IsCpp() ? "auto " : "fn ") << fn.name << "("; + EmitParams(/*is_method=*/false, params, /*indent=*/"", os); + os << ") -> " << fn.return_type << ";\n"; + } +} + // Generates a function declaration and writes it to the provided stream. // // The declaration can be configured with a function name, private modifier, @@ -2085,10 +2421,10 @@ auto SourceGen::GenerateFunctionDecl(ClassGenState& state, llvm::StringRef name, if (out_of_line) { param_count += 1; } - // Exclude class names so that a parameter can't shadow a class that an - // out-of-line definition names. + // Exclude class and free function names so that a parameter can't shadow one + // that an out-of-line definition names. UniqueIdentifierPopper unique_param_names(*this, state.param_names(), - &state.class_name_set()); + &state.file_scope_name_set()); FunctionSig sig = {.name = name, .is_method = is_method}; sig.params.reserve(param_count); sig.param_consumers.reserve(param_count); @@ -2252,21 +2588,24 @@ static auto ArrangeDef(const DefPlan& plan, absl::BitGen& rng) -> ArrangedDef { return def; } -// Emits the steps of `def` after the accumulator declaration that every -// function begins with, indented beyond `indent`. Locals take their names from -// `locals` in turn, class-typed locals take their classes from -// `class_type_local_types` (with `""` for `Self`), the `ReadParams` step adds -// `reads`, and guards return `return_value`. +// Arranges and emits the steps of `state.def_plans()[def_plan]` after the +// accumulator declarations that every function begins with, indented beyond +// `indent`. Locals take their names from `locals` in turn, the `ReadParams` +// step adds `reads`, `Call` and `DestructureCall` steps draw calls from +// `state`, and guards return `return_value`. // // The first operand of each step after a local declaration reads the locals // that it declares, so that every local is read. Every other operand is a // random `i32` local in scope, and the target of each update a random `var` // one. -static auto EmitArrangedDef( - const ArrangedDef& def, llvm::ArrayRef locals, - llvm::ArrayRef class_type_local_types, - llvm::StringRef reads, llvm::StringRef return_value, bool is_cpp, - llvm::StringRef indent, absl::BitGen& rng, llvm::raw_ostream& os) -> void { +auto SourceGen::EmitArrangedDef(ClassGenState& state, int def_plan, + llvm::ArrayRef locals, + llvm::StringRef reads, + llvm::StringRef return_value, + llvm::StringRef indent, llvm::raw_ostream& os) + -> void { + const DefPlan& plan = state.def_plans()[def_plan]; + ArrangedDef def = ArrangeDef(plan, rng_); // The `i32` locals in scope that later steps can update, and the ones that // they can only read. llvm::SmallVector var_scope = {"acc"}; @@ -2276,13 +2615,14 @@ static auto EmitArrangedDef( int next_local = 0; int next_class_type_local = 0; auto pick_local = [&]() { - int index = absl::Uniform(rng, 0, var_scope.size() + let_scope.size()); + int index = + absl::Uniform(rng_, 0, var_scope.size() + let_scope.size()); return index < static_cast(var_scope.size()) ? var_scope[index] : let_scope[index - var_scope.size()]; }; auto pick_var = [&]() { - return var_scope[absl::Uniform(rng, 0, var_scope.size())]; + return var_scope[absl::Uniform(rng_, 0, var_scope.size())]; }; auto emit_expr_to = [&](llvm::StringRef format, llvm::raw_ostream& out) { while (!format.empty()) { @@ -2296,6 +2636,29 @@ static auto EmitArrangedDef( } }; auto emit_expr = [&](llvm::StringRef format) { emit_expr_to(format, os); }; + // Substitutes the locals `a` and `b` into `format`. + auto substitute = [](llvm::StringRef format, llvm::StringRef a, + llvm::StringRef b) { + std::string text = format.str(); + for (auto [placeholder, name] : {std::pair{"{a}", a}, {"{b}", b}}) { + text.replace(text.find(placeholder), 3, name); + } + return text; + }; + auto emit_destructure = [&](const DestructureForm& form) { + llvm::StringRef a = locals[next_local++]; + llvm::StringRef b = locals[next_local++]; + emit_expr(substitute(IsCpp() ? form.cpp_decl : form.carbon_decl, a, b)); + os << "\n"; + auto& scope = form.is_var ? var_scope : let_scope; + if (form.a_is_i32) { + scope.push_back(a); + } + if (form.b_is_i32) { + scope.push_back(b); + } + pending = substitute(IsCpp() ? form.cpp_read : form.carbon_read, a, b); + }; auto emit_cond = [&](int spelling_index) { emit_expr(PlanConds[spelling_index % std::size(PlanConds)]); }; @@ -2359,8 +2722,8 @@ static auto EmitArrangedDef( switch (planned.kind) { case PlanStepKind::ScalarLocal: { llvm::StringRef name = locals[next_local++]; - os << (is_cpp ? "int " : "var ") << name - << (is_cpp ? " = " : ": i32 = "); + os << (IsCpp() ? "int " : "var ") << name + << (IsCpp() ? " = " : ": i32 = "); emit_expr(PlanExprs[planned.spelling_index]); os << ";\n"; var_scope.push_back(name); @@ -2369,11 +2732,12 @@ static auto EmitArrangedDef( } case PlanStepKind::ClassTypeLocal: { llvm::StringRef name = locals[next_local++]; - llvm::StringRef type = class_type_local_types[next_class_type_local++]; + llvm::StringRef type = + plan.class_type_local_types[next_class_type_local++]; if (type.empty()) { - os << (is_cpp ? "Self " : "var ") << name - << (is_cpp ? " = Self();\n" : ": Self = Make();\n"); - } else if (is_cpp) { + os << (IsCpp() ? "Self " : "var ") << name + << (IsCpp() ? " = Self();\n" : ": Self = Make();\n"); + } else if (IsCpp()) { os << type << " " << name << " = " << type << "();\n"; } else { os << "var " << name << ": " << type << " = " << type << ".Make();\n"; @@ -2383,27 +2747,35 @@ static auto EmitArrangedDef( pending = read.TakeStr(); break; } - case PlanStepKind::Destructure: { - const DestructureForm& form = DestructureForms[planned.spelling_index]; + case PlanStepKind::Destructure: + emit_destructure(DestructureForms[planned.spelling_index]); + break; + case PlanStepKind::DestructureCall: { + std::string call; + const CallDestructureForm* form = state.NextDestructureCall(call); + if (!form) { + emit_destructure(DestructureForms[planned.spelling_index]); + break; + } llvm::StringRef a = locals[next_local++]; llvm::StringRef b = locals[next_local++]; - auto substitute = [&](llvm::StringRef format) { - std::string text = format.str(); - for (auto [placeholder, name] : {std::pair{"{a}", a}, {"{b}", b}}) { - text.replace(text.find(placeholder), 3, name); - } - return text; - }; - emit_expr(substitute(is_cpp ? form.cpp_decl : form.carbon_decl)); - os << "\n"; - auto& scope = form.is_var ? var_scope : let_scope; - if (form.a_is_i32) { - scope.push_back(a); + if (IsCpp()) { + os << "const auto [" << a << ", " << b << "]"; + } else { + os << substitute(form->carbon_pattern, a, b); } - if (form.b_is_i32) { - scope.push_back(b); + os << " = " << call << ";\n"; + RawStringOstream read; + emit_expr_to( + substitute(IsCpp() ? form->cpp_read : form->carbon_read, a, b), + read); + if (form->a_is_i32) { + let_scope.push_back(a); } - pending = substitute(is_cpp ? form.cpp_read : form.carbon_read); + if (form->b_is_i32) { + let_scope.push_back(b); + } + pending = read.TakeStr(); break; } case PlanStepKind::Update: @@ -2416,6 +2788,13 @@ static auto EmitArrangedDef( emit_expr("{0}"); os << " + " << reads << ";\n"; break; + case PlanStepKind::Call: + os << pick_var() << " = "; + emit_expr("{0}"); + os << " + "; + state.EmitCall(os); + os << ";\n"; + break; case PlanStepKind::If: case PlanStepKind::IfElse: case PlanStepKind::ElseIf: @@ -2462,11 +2841,24 @@ static auto EmitArrangedDef( // types, inline definitions only use classes defined prior to the enclosing // class, whereas out-of-line definitions follow every class and can use any // other class; see `GetValidReturnType`. +// +// A definition with calls or destructured calls first declares `acc64`, which +// pointer arguments can point at along with `acc`, and initializes `acc` from +// it so that it is read. auto SourceGen::GenerateFunctionDef(ClassGenState& state, const FunctionSig& sig, llvm::StringRef indent, llvm::raw_ostream& os) -> void { - os << indent << (IsCpp() ? " int acc = 0;\n" : " var acc: i32 = 0;\n"); + const DefPlan& plan = state.def_plans()[sig.def_plan]; + if (plan.calls + plan.destructure_calls == 0) { + os << indent << (IsCpp() ? " int acc = 0;\n" : " var acc: i32 = 0;\n"); + } else if (!IsCpp()) { + os << indent << " var acc64: i64 = 0;\n"; + os << indent << " var acc: i32 = acc64 as i32;\n"; + } else { + os << indent << " std::int64_t acc64 = 0;\n"; + os << indent << " int acc = static_cast(acc64);\n"; + } RawStringOstream reads; llvm::ListSeparator read_sep(" + "); @@ -2481,7 +2873,6 @@ auto SourceGen::GenerateFunctionDef(ClassGenState& state, // Locals and parameters draw from the same identifiers of each length, so // exclude this function's parameter names: C++ rejects a local that // has the same name as a parameter. - const DefPlan& plan = state.def_plans()[sig.def_plan]; Set param_names; for (const TypedName& param : sig.params) { param_names.Insert(param.name); @@ -2497,8 +2888,8 @@ auto SourceGen::GenerateFunctionDef(ClassGenState& state, RawStringOstream return_value; state.ProduceValue(sig.return_type, return_value); std::string return_value_str = return_value.TakeStr(); - EmitArrangedDef(ArrangeDef(plan, rng_), locals, plan.class_type_local_types, - reads.TakeStr(), return_value_str, IsCpp(), indent, rng_, os); + EmitArrangedDef(state, sig.def_plan, locals, reads.TakeStr(), + return_value_str, indent, os); os << indent << " return " << return_value_str << ";\n"; } @@ -2549,10 +2940,10 @@ auto SourceGen::GenerateInlineFunctionDef(ClassGenState& state, // Add the extra parameter; see `BuildClassAndTypeNames`. param_count += 1; - // Exclude class names so that a parameter can't shadow a class that the - // definition names. + // Exclude class and free function names so that a parameter can't shadow one + // that the definition names. UniqueIdentifierPopper unique_param_names(*this, state.inline_param_names(), - &state.class_name_set()); + &state.file_scope_name_set()); FunctionSig sig = {.name = name, .is_method = false}; sig.params.reserve(param_count); sig.param_consumers.reserve(param_count); @@ -2701,7 +3092,7 @@ auto SourceGen::GenerateClassDef( // Name the getter and predicate fields first, so that member names can avoid // them. UniqueIdentifierPopper unique_accessed_names( - *this, state.accessed_field_names(), &state.class_name_set()); + *this, state.accessed_field_names(), &state.file_scope_name_set()); llvm::SmallVector getter_fields; getter_fields.reserve(params.inline_getters); for (auto _ : llvm::seq(params.inline_getters)) { @@ -2719,10 +3110,11 @@ auto SourceGen::GenerateClassDef( field.predicate}); } - // Definitions in this class can name any class, so exclude class names from - // member names. Inline function names are too short to be class names. + // Definitions in this class can name any class or free function, so exclude + // their names from member names. Inline function names are too short to be + // one. UniqueIdentifierPopper unique_member_names(*this, state.decl_names(), - &state.class_name_set()); + &state.file_scope_name_set()); unique_member_names.Reserve(unique_accessed_names.used()); UniqueIdentifierPopper unique_inline_names(*this, state.inline_function_names()); @@ -2781,10 +3173,10 @@ auto SourceGen::GenerateClassDef( for (auto _ : llvm::seq(params.inline_forwarders)) { Forwarder& forwarder = forwarders.emplace_back(); forwarder.name = unique_forwarder_names.Pop(); - // Exclude class names so that a parameter can't shadow a class in the - // signature. + // Exclude class and free function names so that a parameter can't shadow a + // class in the signature. UniqueIdentifierPopper unique_param_names( - *this, state.forwarder_param_names(), &state.class_name_set()); + *this, state.forwarder_param_names(), &state.file_scope_name_set()); for (auto _ : llvm::seq(state.forwarder_param_counts().pop_back_val())) { forwarder.params.push_back({.name = unique_param_names.Pop(), .type = state.GetForwardType().name}); @@ -2823,9 +3215,9 @@ auto SourceGen::GenerateClassDef( } // Field names can't repeat this class's function, method, or field names, or - // be a class name. + // be a class or free function name. UniqueIdentifierPopper unique_field_names(*this, state.field_names(), - &state.class_name_set()); + &state.file_scope_name_set()); unique_field_names.Reserve(unique_member_names.used()); llvm::SmallVector fields; fields.reserve(field_type_names.size() + getter_fields.size() + diff --git a/toolchain/benchmarking/source_gen.h b/toolchain/benchmarking/source_gen.h index c88e6072b489..5c357e3b0fab 100644 --- a/toolchain/benchmarking/source_gen.h +++ b/toolchain/benchmarking/source_gen.h @@ -181,6 +181,12 @@ class SourceGen { // `{0}` placeholder for a value of the type. Only types with a predicate // template and a value expression are used for the fields that predicates // test. + // + // Each type can also have an argument expression, which definitions pass + // for parameters of the type when calling free functions. It defaults to + // the value expression, so only types without a value expression need to + // specify one. It can use the definition's `acc` (an `i32`) and `acc64` + // (an `i64`). struct FixedTypeWeight { llvm::StringRef carbon_spelling; llvm::StringRef cpp_spelling; @@ -191,9 +197,13 @@ class SourceGen { llvm::SmallVector cpp_consumers; llvm::StringRef carbon_predicate; llvm::StringRef cpp_predicate; + llvm::StringRef carbon_arg; + llvm::StringRef cpp_arg; }; - llvm::SmallVector fixed_type_weights = { + // `FixedTypeWeight` exceeds `SmallVector`'s 256-byte limit for default + // inline storage. + llvm::SmallVector fixed_type_weights = { // Combined weight of 65 for a core set of builtin types. {.carbon_spelling = "bool", .cpp_spelling = "bool", @@ -228,13 +238,17 @@ class SourceGen { .cpp_spelling = "int*", .weight = 5, .carbon_consumers = {"*{0} + 1", "*{0} * 2"}, - .cpp_consumers = {"*{0} + 1", "*{0} * 2"}}, + .cpp_consumers = {"*{0} + 1", "*{0} * 2"}, + .carbon_arg = "&acc", + .cpp_arg = "&acc"}, {.carbon_spelling = "i64*", .cpp_spelling = "std::int64_t*", .weight = 5, .carbon_consumers = {"(*{0} as i32) - 2", "((*{0} * 2) as i32)"}, .cpp_consumers = {"static_cast(*{0}) - 2", - "static_cast(*{0} * 2)"}}, + "static_cast(*{0} * 2)"}, + .carbon_arg = "&acc64", + .cpp_arg = "&acc64"}, // A weight of 5 distributed across tuple structures {.carbon_spelling = "(bool, i64)", @@ -252,7 +266,9 @@ class SourceGen { .weight = 3, .carbon_consumers = {"{0}.0 + 1", "{0}.0 * 2", "(*{0}.1 as i32) - 1"}, .cpp_consumers = {"{0}.first + 1", "{0}.first * 2", - "static_cast(*{0}.second) - 1"}}, + "static_cast(*{0}.second) - 1"}, + .carbon_arg = "(0, &acc64)", + .cpp_arg = "{0, &acc64}"}, }; // Consumer templates for class types, in both languages. These call the @@ -298,6 +314,12 @@ class SourceGen { {.statements = 29, .weight = 3}, {.statements = 43, .weight = 2}, {.statements = 67, .weight = 1}, {.statements = 125, .weight = 1}, }; + + // The number of free function declarations at the start of the file, per + // class. Definitions call these. They precede the classes, so their + // signatures only use fixed types. + int free_function_decls_per_class = 0; + FunctionDeclParams free_function_decl_params = {.max_params = 4}; }; // Access a global instance of this type to generate Carbon code for @@ -443,6 +465,12 @@ class SourceGen { bool is_private, bool is_method, int param_count, llvm::StringRef indent, llvm::raw_ostream& os, FunctionSig* captured = nullptr) -> void; + auto GenerateFreeFunctionDecls(ClassGenState& state, llvm::raw_ostream& os) + -> void; + auto EmitArrangedDef(ClassGenState& state, int def_plan, + llvm::ArrayRef locals, + llvm::StringRef reads, llvm::StringRef return_value, + llvm::StringRef indent, llvm::raw_ostream& os) -> void; auto GenerateFunctionDef(ClassGenState& state, const FunctionSig& sig, llvm::StringRef indent, llvm::raw_ostream& os) -> void; diff --git a/toolchain/benchmarking/source_gen_test.cpp b/toolchain/benchmarking/source_gen_test.cpp index a6db087b59db..ce7f4a9e8055 100644 --- a/toolchain/benchmarking/source_gen_test.cpp +++ b/toolchain/benchmarking/source_gen_test.cpp @@ -36,6 +36,7 @@ using ::testing::Gt; using ::testing::HasSubstr; using ::testing::Le; using ::testing::MatchesRegex; +using ::testing::Not; using ::testing::SizeIs; using ::testing::StrEq; using ::testing::TestParamInfo; @@ -624,43 +625,144 @@ TEST(SourceGenTest, GenApiFileDenseDeclsPlanDeclarations) { EXPECT_THAT(source, HasSubstr("auto [")); } +// Definitions call free functions, passing `&acc` and `&acc64` for pointer +// parameters and destructuring tuple-returning calls. +TEST_P(GenApiFileDenseDeclsParamsTest, FreeFunctionCalls) { + SourceGen::DenseDeclParams params = { + .class_params = {.inline_function_defs = 2}, + .define_decls_out_of_line = true, + .free_function_decls_per_class = 2}; + EXPECT_THAT(GetParam(), GeneratesSeedIndependentSize(3000, params, 16)); + std::string source = SourceGen(GetParam()).GenApiFileDenseDecls(3000, params); + EXPECT_TRUE(TestCompile(GetParam(), source)); + EXPECT_THAT(source, HasSubstr("&acc")); + EXPECT_THAT(source, HasSubstr("&acc64")); +} + +// When no free function returns a tuple in `CallDestructureForms`, +// `DestructureCall` steps fall back to literal destructuring while keeping file +// sizes seed-independent. +TEST_P(GenApiFileDenseDeclsParamsTest, FreeFunctionCallsWithoutTupleReturns) { + SourceGen::DenseDeclParams params = { + .class_params = {.public_function_decls = 1, + .public_method_decls = 1, + .private_function_decls = 0, + .private_method_decls = 0, + .private_field_decls = 2}, + .define_decls_out_of_line = true, + .out_of_line_def_sizes = {{.statements = 60, .weight = 1}}, + .free_function_decls_per_class = 2}; + // Drop the tuple entries at the end of `fixed_type_weights` so no free + // function returns a destructurable tuple. + llvm::erase_if(params.type_use_params.fixed_type_weights, + [](const SourceGen::TypeUseParams::FixedTypeWeight& fw) { + return fw.carbon_spelling.starts_with("("); + }); + EXPECT_THAT(GetParam(), GeneratesSeedIndependentSize(3000, params, 16)); + std::string source = SourceGen(GetParam()).GenApiFileDenseDecls(3000, params); + EXPECT_TRUE(TestCompile(GetParam(), source)); + EXPECT_THAT(source, HasSubstr("&acc64")); + if (GetParam() == SourceGen::Language::Carbon) { + EXPECT_THAT(source, + ContainsRegex("(let|var) \\([A-Za-z0-9_]+: [^)]+\\) = \\(")); + EXPECT_THAT(source, Not(ContainsRegex("let \\([^)]+\\) = [A-Za-z]"))); + } else { + EXPECT_THAT(source, HasSubstr("] = std::pair{")); + EXPECT_THAT( + source, + Not(ContainsRegex("auto \\[[A-Za-z0-9_, ]+\\] = [A-Za-z0-9_]+\\("))); + } +} + +TEST(SourceGenTest, GenApiFileDenseDeclsFreeFunctionCalls) { + SourceGen::DenseDeclParams params = { + .class_params = {.public_function_decls = 1, + .public_method_decls = 1, + .private_function_decls = 0, + .private_method_decls = 0, + .private_field_decls = 2}, + .define_decls_out_of_line = true, + .out_of_line_def_sizes = {{.statements = 60, .weight = 1}}, + .free_function_decls_per_class = 4}; + + auto carbon_destructure_call_regex = [](llvm::StringRef first_type, + llvm::StringRef second_type) { + return llvm::formatv( + "let \\([A-Za-z0-9_]+: {0}, [A-Za-z0-9_]+: {1}\\) = " + "[A-Za-z0-9_]{{5,}\\(", + first_type, second_type) + .str(); + }; + + SourceGen carbon_gen; + std::string source = carbon_gen.GenApiFileDenseDecls(3000, params); + auto [carbon_free_decls, carbon_classes_and_defs] = + llvm::StringRef(source).split("\nclass "); + EXPECT_THAT(carbon_free_decls, ContainsRegex("\nfn [A-Za-z0-9_]{5,}\\(")); + EXPECT_THAT(carbon_classes_and_defs, + ContainsRegex(carbon_destructure_call_regex("bool", "i64"))); + EXPECT_THAT(carbon_classes_and_defs, + ContainsRegex(carbon_destructure_call_regex("i32", "i64\\*"))); + + SourceGen cpp_gen(SourceGen::Language::Cpp); + source = cpp_gen.GenApiFileDenseDecls(3000, params); + auto [cpp_free_decls, cpp_classes_and_defs] = + llvm::StringRef(source).split("\nclass "); + EXPECT_THAT(cpp_free_decls, ContainsRegex("\nauto [A-Za-z0-9_]{5,}\\(")); + EXPECT_THAT(cpp_classes_and_defs, + ContainsRegex("const auto \\[[A-Za-z0-9_]+, [A-Za-z0-9_]+\\] = " + "[A-Za-z0-9_]{5,}\\(")); +} + // The line estimates have to track the emitted lines closely, or files miss // their target size. The target is large so that rounding to a whole number of // classes is small next to the tolerance. C++ gets a larger tolerance for its // access specifier lines, which the estimates don't count. TEST(SourceGenTest, GenApiFileDenseDeclsLineTargetAccuracy) { - llvm::SmallVector param_set; - param_set.push_back({.class_params = {.inline_function_defs = 1, - .inline_getters = 1, - .inline_predicates = 1, - .inline_forwarders = 1}}); + struct NamedParams { + llvm::StringLiteral name; + SourceGen::DenseDeclParams params; + }; + llvm::SmallVector param_set; + param_set.push_back({.name = "dense_decls", + .params = {.class_params = {.inline_function_defs = 1, + .inline_getters = 1, + .inline_predicates = 1, + .inline_forwarders = 1}, + .free_function_decls_per_class = 2}}); // The implementation file shape. param_set.push_back( - {.class_params = {.public_function_decls = 2, - .public_function_decl_params = {.max_params = 2}, - .public_method_decls = 6, - .public_method_decl_params = {.max_params = 2}, - .private_function_decls = 2, - .private_function_decl_params = {.max_params = 2}, - .private_method_decls = 2, - .private_method_decl_params = {.max_params = 2}, - .private_field_decls = 4}, - .define_decls_out_of_line = true}); + {.name = "impl_file", + .params = { + .class_params = {.public_function_decls = 2, + .public_function_decl_params = {.max_params = 2}, + .public_method_decls = 6, + .public_method_decl_params = {.max_params = 2}, + .private_function_decls = 2, + .private_function_decl_params = {.max_params = 2}, + .private_method_decls = 2, + .private_method_decl_params = {.max_params = 2}, + .private_field_decls = 4}, + .define_decls_out_of_line = true, + .free_function_decls_per_class = 2}}); // The realistic header shape. param_set.push_back( - {.class_params = {.public_function_decls = 0, - .public_method_decls = 2, - .private_function_decls = 0, - .private_method_decls = 0, - .private_field_decls = 3, - .inline_function_defs = 2, - .inline_function_decl_params = {.max_params = 2}, - .inline_getters = 2, - .inline_predicates = 1, - .inline_forwarders = 2}}); + {.name = "realistic_header", + .params = { + .class_params = {.public_function_decls = 0, + .public_method_decls = 2, + .private_function_decls = 0, + .private_method_decls = 0, + .private_field_decls = 3, + .inline_function_defs = 2, + .inline_function_decl_params = {.max_params = 2}, + .inline_getters = 2, + .inline_predicates = 1, + .inline_forwarders = 2}, + .free_function_decls_per_class = 1}}); constexpr int TargetLines = 20000; - for (const SourceGen::DenseDeclParams& params : param_set) { + for (const auto& [name, params] : param_set) { for (SourceGen::Language language : {SourceGen::Language::Carbon, SourceGen::Language::Cpp}) { SourceGen gen(language); @@ -668,12 +770,10 @@ TEST(SourceGenTest, GenApiFileDenseDeclsLineTargetAccuracy) { ssize_t lines = CountLines(source); if (language == SourceGen::Language::Carbon) { // Within 2% of the requested line count. - EXPECT_THAT(lines, AllOf(Ge(19600), Le(20400))) - << "define_decls_out_of_line=" << params.define_decls_out_of_line; + EXPECT_THAT(lines, AllOf(Ge(19600), Le(20400))) << name; } else { // Within 10% of the requested line count. - EXPECT_THAT(lines, AllOf(Ge(18000), Le(22000))) - << "define_decls_out_of_line=" << params.define_decls_out_of_line; + EXPECT_THAT(lines, AllOf(Ge(18000), Le(22000))) << name; } } }