From 80092176fc170a2592784201fbe0be90ddb36527 Mon Sep 17 00:00:00 2001 From: Pranjal Raihan Date: Sat, 11 Jan 2025 02:19:59 -0800 Subject: [PATCH] standard library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This diff introduces the "Whisker standard library" — a place to put *extremely* generic functionality around template data. The initial implementation just has the following functions: - `array.of(...)` — array literals - `array.len` - `array.empty?` - `str.len` I picked a small set of (non-controversial) functions just to validate the approach. Reviewed By: yoney Differential Revision: D67958812 fbshipit-source-id: 36200dc21c1f053ad31664d8d3ee562ab641109a --- .../thrift/src/thrift/compiler/CMakeLists.txt | 1 + .../compiler/generate/t_mstch_generator.cc | 3 +- .../compiler/whisker/standard_library.cc | 166 ++++++++++++++++++ .../compiler/whisker/standard_library.h | 31 ++++ .../whisker/test/render_test_helpers.h | 11 +- .../whisker/test/standard_library_test.cc | 121 +++++++++++++ 6 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 third-party/thrift/src/thrift/compiler/whisker/standard_library.cc create mode 100644 third-party/thrift/src/thrift/compiler/whisker/standard_library.h create mode 100644 third-party/thrift/src/thrift/compiler/whisker/test/standard_library_test.cc diff --git a/third-party/thrift/src/thrift/compiler/CMakeLists.txt b/third-party/thrift/src/thrift/compiler/CMakeLists.txt index 6a33a421d6f397..bbca135bed9d36 100644 --- a/third-party/thrift/src/thrift/compiler/CMakeLists.txt +++ b/third-party/thrift/src/thrift/compiler/CMakeLists.txt @@ -181,6 +181,7 @@ add_library( whisker/parser.cc whisker/print_ast.cc whisker/render.cc + whisker/standard_library.cc whisker/token.cc whisker/tree_printer.cc ) diff --git a/third-party/thrift/src/thrift/compiler/generate/t_mstch_generator.cc b/third-party/thrift/src/thrift/compiler/generate/t_mstch_generator.cc index fc954bf66fc7ef..353c9f0ea9d248 100644 --- a/third-party/thrift/src/thrift/compiler/generate/t_mstch_generator.cc +++ b/third-party/thrift/src/thrift/compiler/generate/t_mstch_generator.cc @@ -16,7 +16,6 @@ #include -#include #include #include #include @@ -34,6 +33,7 @@ #include #include #include +#include #include #include @@ -559,6 +559,7 @@ t_mstch_generator::gen_whisker_render_state(whisker_options whisker_opts) { render_options.strict_printable_types = whisker::diagnostic_level::debug; render_options.strict_undefined_variables = whisker::diagnostic_level::error; + whisker::load_standard_library(render_options.globals); for (const auto& undefined_name : whisker_opts.allowed_undefined_variables) { render_options.globals.insert({undefined_name, whisker::make::null}); } diff --git a/third-party/thrift/src/thrift/compiler/whisker/standard_library.cc b/third-party/thrift/src/thrift/compiler/whisker/standard_library.cc new file mode 100644 index 00000000000000..6783251409c7d8 --- /dev/null +++ b/third-party/thrift/src/thrift/compiler/whisker/standard_library.cc @@ -0,0 +1,166 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace w = whisker::make; + +namespace whisker { + +namespace { + +class named_native_function : public native_function { + public: + explicit named_native_function(std::string_view name) + : name_(std::move(name)) {} + + void print_to( + tree_printer::scope scope, const object_print_options&) const final { + scope.println("", name_); + } + + private: + std::string_view name_; +}; + +map::value_type create_array_functions() { + map array_functions; + + { + /** + * Creates an array with the provided arguments in order. This function can + * be used to form an "array literal". + * + * Name: array.of + * + * Arguments: + * 0 or more objects (variadic) + * + * Returns: + * [array] provided arguments in order. + */ + class array_of : public named_native_function { + public: + array_of() : named_native_function("array.of") {} + + object::ptr invoke(context ctx) override { + ctx.declare_named_arguments({}); + array result; + result.reserve(ctx.arity()); + for (const object::ptr& arg : ctx.arguments()) { + result.emplace_back(object(*arg)); + } + return object::owned(w::array(std::move(result))); + } + }; + array_functions["of"] = w::make_native_function(); + } + + { + /** + * Produces the length of an array or array-like object. + * + * Name: array.len + * + * Arguments: + * - [array] — The array to find length of. + * + * Returns: + * [i64] length of the provided array. + */ + class array_len : public named_native_function { + public: + array_len() : named_native_function("array.len") {} + + object::ptr invoke(context ctx) override { + ctx.declare_named_arguments({}); + ctx.declare_arity(1); + auto len = i64(ctx.argument(0).size()); + return object::owned(w::i64(len)); + } + }; + array_functions["len"] = w::make_native_function(); + } + + { + /** + * Checks an array for emptiness. + * + * Name: array.empty? + * + * Arguments: + * - [array] — The array to check for emptiness. + * + * Returns: + * [boolean] indicating whether the array is empty. + */ + class array_empty : public named_native_function { + public: + array_empty() : named_native_function("array.empty?") {} + + object::ptr invoke(context ctx) override { + ctx.declare_named_arguments({}); + ctx.declare_arity(1); + return object::as_static( + ctx.argument(0).size() == 0 ? w::true_ : w::false_); + } + }; + array_functions["empty?"] = w::make_native_function(); + } + + return map::value_type{"array", std::move(array_functions)}; +} + +map::value_type create_string_functions() { + map string_functions; + + { + /** + * Produces the length of string. + * + * Name: string.len + * + * Arguments: + * - [string] — The string to find length of. + * + * Returns: + * [i64] length of the provided string. + */ + class string_len : public named_native_function { + public: + string_len() : named_native_function("string.len") {} + + object::ptr invoke(context ctx) override { + ctx.declare_named_arguments({}); + ctx.declare_arity(1); + auto len = i64(ctx.argument(0)->length()); + return object::owned(w::i64(len)); + } + }; + string_functions["len"] = w::make_native_function(); + } + + return map::value_type{"string", std::move(string_functions)}; +} + +} // namespace + +void load_standard_library(map& module) { + module.emplace(create_array_functions()); + module.emplace(create_string_functions()); +} + +} // namespace whisker diff --git a/third-party/thrift/src/thrift/compiler/whisker/standard_library.h b/third-party/thrift/src/thrift/compiler/whisker/standard_library.h new file mode 100644 index 00000000000000..1348459e550be4 --- /dev/null +++ b/third-party/thrift/src/thrift/compiler/whisker/standard_library.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace whisker { + +/** + * Loads Whisker's standard library into the provided map. A common use case is + * calling this function on render_options::globals map object. + * + * See standard_library.cc for available functions. + */ +void load_standard_library(map& module); + +} // namespace whisker diff --git a/third-party/thrift/src/thrift/compiler/whisker/test/render_test_helpers.h b/third-party/thrift/src/thrift/compiler/whisker/test/render_test_helpers.h index 27e867e725f8b9..9ef6295c4c035d 100644 --- a/third-party/thrift/src/thrift/compiler/whisker/test/render_test_helpers.h +++ b/third-party/thrift/src/thrift/compiler/whisker/test/render_test_helpers.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -89,6 +90,7 @@ class RenderTest : public testing::Test { std::optional strict_undefined_variables; // Backtraces are disabled by default since they add generally add noise. bool show_source_backtrace_on_failure = false; + std::vector> libraries_to_load; void apply_to(render_options& options) const { if (strict_boolean_conditional.has_value()) { @@ -103,10 +105,14 @@ class RenderTest : public testing::Test { options.show_source_backtrace_on_failure = show_source_backtrace_on_failure ? diagnostic_level::error : diagnostic_level::info; + for (const auto& load : libraries_to_load) { + load(options.globals); + } } }; render_test_options render_test_options_; + protected: void SetUp() override { last_render_ = std::nullopt; render_test_options_ = {}; @@ -125,6 +131,9 @@ class RenderTest : public testing::Test { void show_source_backtrace_on_failure(bool enabled) { render_test_options_.show_source_backtrace_on_failure = enabled; } + void use_library(std::function library_loader) { + render_test_options_.libraries_to_load.push_back(std::move(library_loader)); + } struct partials_by_path { /** @@ -165,7 +174,6 @@ class RenderTest : public testing::Test { } render_options options; - render_test_options_.apply_to(options); if (!partials.value.empty()) { auto partial_resolver = std::make_unique(current.src_manager); @@ -175,6 +183,7 @@ class RenderTest : public testing::Test { options.partial_resolver = std::move(partial_resolver); } options.globals = std::move(globals.value); + render_test_options_.apply_to(options); std::ostringstream out; if (whisker::render( diff --git a/third-party/thrift/src/thrift/compiler/whisker/test/standard_library_test.cc b/third-party/thrift/src/thrift/compiler/whisker/test/standard_library_test.cc new file mode 100644 index 00000000000000..e46c56b356b341 --- /dev/null +++ b/third-party/thrift/src/thrift/compiler/whisker/test/standard_library_test.cc @@ -0,0 +1,121 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include + +namespace w = whisker::make; + +namespace whisker { + +namespace { +class StandardLibraryTest : public RenderTest { + protected: + void SetUp() override { + RenderTest::SetUp(); + use_library(load_standard_library); + } +}; +} // namespace + +TEST_F(StandardLibraryTest, array_of) { + { + auto result = render( + "The factorial function looks like:\n" + "{{#let factorials = (array.of 1 2 6 24 120)}}\n" + "{{#factorials}}\n" + "{{.}}\n" + "{{/factorials}}\n", + w::null); + EXPECT_THAT(diagnostics(), testing::IsEmpty()); + EXPECT_EQ( + *result, + "The factorial function looks like:\n" + "1\n" + "2\n" + "6\n" + "24\n" + "120\n"); + } + + { + strict_boolean_conditional(diagnostic_level::info); + auto result = render( + "{{#let empty = (array.of)}}\n" + "{{^empty}}\n" + "It's empty!\n" + "{{/empty}}\n", + w::null); + EXPECT_THAT(diagnostics(), testing::IsEmpty()); + EXPECT_EQ(*result, "It's empty!\n"); + } +} + +TEST_F(StandardLibraryTest, array_len) { + { + auto result = render( + "{{ (array.len (array.of)) }}\n" + "{{ (array.len (array.of 1 \"foo\" true)) }}\n" + "{{ (array.len (array.of 1 2 6 24 120)) }}\n", + w::null); + EXPECT_THAT(diagnostics(), testing::IsEmpty()); + EXPECT_EQ( + *result, + "0\n" + "3\n" + "5\n"); + } +} + +TEST_F(StandardLibraryTest, array_empty) { + { + strict_printable_types(diagnostic_level::info); + auto result = render( + "{{ (array.empty? (array.of)) }}\n" + "{{ (array.empty? (array.of 1 \"foo\" true)) }}\n" + "{{ (array.empty? (array.of 1 2 6 24 120)) }}\n", + w::null); + EXPECT_THAT(diagnostics(), testing::IsEmpty()); + EXPECT_EQ( + *result, + "true\n" + "false\n" + "false\n"); + } +} + +TEST_F(StandardLibraryTest, string_len) { + { + auto result = render( + "{{ (string.len \"\") }}\n" + "{{ (string.len \"hello\") }}\n" + "{{ (string.len \"\\t\") }}\n", + w::null); + EXPECT_THAT(diagnostics(), testing::IsEmpty()); + EXPECT_EQ( + *result, + "0\n" + "5\n" + "1\n"); + } +} + +} // namespace whisker