From 34ed083cd63b655eb894969a3c96cc2f4108dc7d Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Mon, 18 Mar 2024 08:37:36 -0600 Subject: [PATCH] Finish removing vector (#11) * Vector for all non-string fixes removed * vector completely removed * Only exceptions left are for `substr` * Try to strike a balance with `noexcept` usage * Various clang-tidy related fixes * remove custom iterators * without mixed containers, no longer necessary * Updates to make meetup presentation better :) * Implicit string_view usage? * Add ability to get string_view from cons_expr engine * Move to shared evaluation scratch space * Finish removing function local scratch spaces * Removal of dead code and reformatting * :art: Committing clang-format changes --------- Co-authored-by: Clang Robot --- .clang-tidy | 6 +- examples/compile_test.cpp | 2 +- include/cons_expr/cons_expr.hpp | 575 +++++++++++++++-------------- src/ccons_expr/main.cpp | 11 +- src/cons_expr_cli/main_minimal.cpp | 35 +- test/constexpr_tests.cpp | 31 +- 6 files changed, 325 insertions(+), 335 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index cdfaf8b..219fc86 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -19,10 +19,8 @@ HeaderFilterRegex: '' FormatStyle: none CheckOptions: - - key: readability-identifier-length.IgnoredVariableNames - value: 'x|y|z' - - key: readability-identifier-length.IgnoredParameterNames - value: 'x|y|z' + readability-identifier-length.IgnoredVariableNames: 'x|y|z|id|ch' + readability-identifier-length.IgnoredParameterNames: 'x|y|z|id|ch' diff --git a/examples/compile_test.cpp b/examples/compile_test.cpp index d675518..2ce3575 100644 --- a/examples/compile_test.cpp +++ b/examples/compile_test.cpp @@ -33,7 +33,7 @@ consteval auto make_scripted_function() int main() { - // the kicker here is that this lambda is a full self contained script environment + // the kicker here is that this lambda is a full self-contained script environment // that was all parsed and optimized at compile-time auto func = make_scripted_function(); diff --git a/include/cons_expr/cons_expr.hpp b/include/cons_expr/cons_expr.hpp index 1f65724..82d8e48 100644 --- a/include/cons_expr/cons_expr.hpp +++ b/include/cons_expr/cons_expr.hpp @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2023 Jason Turner +Copyright (c) 2023-2024 Jason Turner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -26,16 +26,19 @@ SOFTWARE. #define CONS_EXPR_HPP #include -#include +#include +#include +#include +#include #include #include #include #include #include #include -#include #include #include +#include #include #include #include @@ -79,6 +82,7 @@ SOFTWARE. // between script and C++ // * C++23 as a minimum // * never thread safe +// * no exceptions or dynamic allocations /// Notes // it's a scheme-like language with a few caveats: @@ -87,12 +91,13 @@ SOFTWARE. // * Pair types don't exist, only lists // * only indices and values are passed, for safety during resize of `values` object // Triviality of types is critical to design and structure of this system +// Triviality lets us greatly simplify the copy/move/forward discussion /// To do // * We probably want some sort of "defragment" at some point // * Add constant folding capability // * Allow functions to be registered as "pure" so they can be folded! -// * make allocator aware + namespace lefticus { @@ -111,13 +116,13 @@ template struct chars } template - [[nodiscard]] static consteval auto str(const char (&input)[Size]) noexcept + [[nodiscard]] static consteval auto str(const char (&input)[Size]) noexcept// NOLINT c-arrays requires(!std::is_same_v) { struct Result { - char_type data[Size]; - constexpr operator std::basic_string_view() { return { data, Size - 1 }; } + char_type data[Size];// NOLINT c-arrays + constexpr operator std::basic_string_view() { return { data, Size - 1 }; }// NOLINT implicit }; Result result; @@ -134,114 +139,71 @@ template> -struct SmallOptimizedVector +struct SmallVector { using size_type = SizeType; + using span_type = SpanType; std::array small; size_type small_size_used = 0; + bool error_state = false; static constexpr auto small_capacity = SmallSize; + [[nodiscard]] constexpr Contained &operator[](size_type index) noexcept { return small[index]; } + [[nodiscard]] constexpr const Contained &operator[](size_type index) const noexcept { return small[index]; } [[nodiscard]] constexpr auto size() const noexcept { return small_size_used; } + [[nodiscard]] constexpr auto begin() const noexcept { return small.begin(); } + [[nodiscard]] constexpr auto begin() noexcept { return small.begin(); } - [[nodiscard]] constexpr Contained &operator[](size_type index) { return small[index]; } - - [[nodiscard]] constexpr const Contained &operator[](size_type index) const { return small[index]; } - - [[nodiscard]] constexpr SpanType view(KeyType range) const + [[nodiscard]] constexpr auto end() const noexcept { - return SpanType{ std::span(small).subspan(range.start, range.size) }; + return std::next(small.begin(), static_cast(small_size_used)); } - template constexpr auto emplace_back(Param &&...param) + [[nodiscard]] constexpr auto end() noexcept { - return insert(Contained{ std::forward(param)... }); + return std::next(small.begin(), static_cast(small_size_used)); } - constexpr size_type insert(Contained obj) + [[nodiscard]] constexpr SpanType view(KeyType range) const noexcept { - // handle case where small_size is too small to hold this object - small[small_size_used] = std::move(obj); - return small_size_used++; + return SpanType{ std::span(small).subspan(range.start, range.size) }; } + [[nodiscard]] constexpr auto operator[](KeyType span) const noexcept { return view(span); } - constexpr auto small_end() { return std::next(small.begin(), static_cast(small_size_used)); } + constexpr void push_back(auto &¶m) noexcept { insert(param); } + constexpr void emplace_back(auto &&...param) noexcept { insert(Contained{ param... }); } - struct Iterator + constexpr void resize(SizeType new_size) noexcept { - using difference_type = std::ptrdiff_t; - using value_type = Contained; - using pointer = const Contained *; - using reference = const Contained &; - using iterator_category = std::bidirectional_iterator_tag; - - const SmallOptimizedVector *container; - size_type index; - - [[nodiscard]] constexpr bool operator==(const Iterator &other) const noexcept { return index == other.index; } - [[nodiscard]] constexpr bool operator!=(const Iterator &) const noexcept = default; - - constexpr const auto &operator*() const noexcept { return (*container)[index]; } - constexpr auto &operator++() noexcept - { - ++index; - return *this; - } - [[nodiscard]] constexpr auto operator++(int) noexcept - { - auto result = *this; - ++(*this); - return result; - } - - constexpr auto &operator--() noexcept - { - --index; - return *this; - } - - [[nodiscard]] constexpr auto operator--(int) noexcept - { - auto result = *this; - --(*this); - return result; - } - }; + small_size_used = std::min(new_size, SmallSize); + if (new_size > SmallSize) { error_state = true; } + } - struct View + constexpr size_type insert(Contained obj) noexcept { - KeyType span; - const SmallOptimizedVector *container; - [[nodiscard]] constexpr const auto &operator[](size_type offset) const noexcept - { - return (*container)[span.start + offset]; - } - [[nodiscard]] constexpr auto empty() const noexcept { return span.empty(); } - [[nodiscard]] constexpr auto size() const noexcept { return span.size; } - [[nodiscard]] constexpr auto begin() const noexcept { return Iterator{ container, span.start }; } - [[nodiscard]] constexpr auto end() const noexcept - { - return Iterator{ container, static_cast(span.start + span.size) }; + if (small_size_used < small_capacity) { + small[small_size_used] = std::move(obj); + return small_size_used++; + } else { + error_state = true; + return small_size_used; } - }; + } - [[nodiscard]] constexpr auto operator[](KeyType span) const noexcept { return View{ span, this }; } - [[nodiscard]] constexpr auto begin() const noexcept { return Iterator{ this, 0 }; } - [[nodiscard]] constexpr auto end() const noexcept { return Iterator{ this, size() }; } - constexpr KeyType insert_or_find(SpanType values) + constexpr KeyType insert_or_find(SpanType values) noexcept { - if (const auto small_found = std::search(small.begin(), small_end(), values.begin(), values.end()); - small_found != small_end()) { - return KeyType{ static_cast(std::distance(small.begin(), small_found)), + if (const auto small_found = std::search(begin(), end(), values.begin(), values.end()); small_found != end()) { + return KeyType{ static_cast(std::distance(begin(), small_found)), static_cast(values.size()) }; } else { return insert(values); } } - constexpr KeyType insert(SpanType values) + constexpr KeyType insert(SpanType values) noexcept { size_type last = 0; for (const auto &value : values) { last = insert(value); } @@ -251,26 +213,19 @@ struct SmallOptimizedVector template concept not_bool_or_ptr = !std::same_as, bool> && !std::is_pointer_v>; -template -concept addable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs + rhs; }; -template -concept multipliable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs *rhs; }; -template -concept dividable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs / rhs; }; -template -concept subtractable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs - rhs; }; -template -concept lt_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs < rhs; }; -template -concept gt_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs > rhs; }; -template -concept lt_eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs <= rhs; }; -template -concept gt_eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs >= rhs; }; -template -concept eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs == rhs; }; -template -concept not_eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs != rhs; }; + +// clang-format off +template concept addable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs + rhs; }; +template concept multipliable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs *rhs; }; +template concept dividable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs / rhs; }; +template concept subtractable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs - rhs; }; +template concept lt_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs < rhs; }; +template concept gt_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs > rhs; }; +template concept lt_eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs <= rhs; }; +template concept gt_eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs >= rhs; }; +template concept eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs == rhs; }; +template concept not_eq_comparable = not_bool_or_ptr && requires(T lhs, T rhs) { lhs != rhs; }; +// clang-format on inline constexpr auto adds = [](const T &lhs, const T &rhs) { return lhs + rhs; }; inline constexpr auto multiplies = [](const T &lhs, const T &rhs) { return lhs * rhs; }; @@ -296,12 +251,12 @@ template Token(std::basic_string_view, std::basic_string_view) -> Token; template -[[nodiscard]] constexpr std::pair parse_number(std::basic_string_view input) +[[nodiscard]] constexpr std::pair parse_number(std::basic_string_view input) noexcept { static constexpr std::pair failure{ false, 0 }; if (input == chars::str("-")) { return failure; } - enum struct State { + enum struct State : std::uint8_t { Start, IntegerPart, FractionPart, @@ -309,99 +264,83 @@ template ExponentStart, }; - struct ParseState - { - State state = State::Start; - T value_sign = 1; - long long value = 0LL; - long long frac = 0LL; - long long frac_exp = 0LL; - long long exp_sign = 1LL; - long long exp = 0LL; - - [[nodiscard]] static constexpr auto pow(const T base, long long power) noexcept - { - auto result = decltype(base)(1); - if (power > 0) { - for (int iteration = 0; iteration < power; ++iteration) { result *= base; } - } else if (power < 0) { - for (int iteration = 0; iteration > power; --iteration) { result /= base; } - } - return result; - }; - - [[nodiscard]] constexpr auto float_value() const noexcept -> std::pair - { - if (state == State::Start || state == State::ExponentStart) { return { false, 0 }; } - - return { true, - (static_cast(value_sign) * (static_cast(value) + static_cast(frac) * pow(static_cast(10), frac_exp)) - * pow(static_cast(10), exp_sign * exp)) }; - } - - [[nodiscard]] constexpr auto int_value() const noexcept -> std::pair - { - return { true, value_sign * static_cast(value) }; + State state = State::Start; + T value_sign = 1; + long long value = 0LL; + long long frac = 0LL; + long long frac_exp = 0LL; + long long exp_sign = 1LL; + long long exp = 0LL; + + constexpr auto pow_10 = [](long long power) noexcept { + auto result = T{ 1 }; + if (power > 0) { + for (int iteration = 0; iteration < power; ++iteration) { result *= T{ 10 }; } + } else if (power < 0) { + for (int iteration = 0; iteration > power; --iteration) { result /= T{ 10 }; } } + return result; }; - ParseState state; - - auto parse_digit = [](auto &value, auto ch) { + const auto parse_digit = [](auto &cur_value, auto ch) { if (ch >= chars::ch('0') && ch <= chars::ch('9')) { - value = value * 10 + ch - chars::ch('0'); + cur_value = cur_value * 10 + ch - chars::ch('0'); return true; } else { return false; } }; - for (const auto c : input) { - switch (state.state) { + for (const auto ch : input) { + switch (state) { case State::Start: - if (c == chars::ch('-')) { - state.value_sign = -1; - } else if (!parse_digit(state.value, c)) { + if (ch == chars::ch('-')) { + value_sign = -1; + } else if (!parse_digit(value, ch)) { return failure; } - state.state = State::IntegerPart; + state = State::IntegerPart; break; case State::IntegerPart: - if (c == chars::ch('.')) { - state.state = State::FractionPart; - } else if (c == chars::ch('e') || c == chars::ch('E')) { - state.state = State::ExponentPart; - } else if (!parse_digit(state.value, c)) { + if (ch == chars::ch('.')) { + state = State::FractionPart; + } else if (ch == chars::ch('e') || ch == chars::ch('E')) { + state = State::ExponentPart; + } else if (!parse_digit(value, ch)) { return failure; } break; case State::FractionPart: - if (parse_digit(state.frac, c)) { - state.frac_exp--; - } else if (c == chars::ch('e') || c == chars::ch('E')) { - state.state = State::ExponentStart; + if (parse_digit(frac, ch)) { + frac_exp--; + } else if (ch == chars::ch('e') || ch == chars::ch('E')) { + state = State::ExponentStart; } else { return failure; } break; case State::ExponentStart: - if (c == chars::ch('-')) { - state.exp_sign = -1; - } else if (!parse_digit(state.exp, c)) { + if (ch == chars::ch('-')) { + exp_sign = -1; + } else if (!parse_digit(exp, ch)) { return failure; } - state.state = State::ExponentPart; + state = State::ExponentPart; break; case State::ExponentPart: - if (!parse_digit(state.exp, c)) { return failure; } + if (!parse_digit(exp, ch)) { return failure; } } } if constexpr (std::is_integral_v) { - if (state.state != State::IntegerPart) { return failure; } - return state.int_value(); + if (state != State::IntegerPart) { return failure; } + return { true, value_sign * static_cast(value) }; } else { - return state.float_value(); + if (state == State::Start || state == State::ExponentStart) { return { false, 0 }; } + + return { true, + (static_cast(value_sign) * (static_cast(value) + static_cast(frac) * pow_10(frac_exp)) + * pow_10(exp_sign * exp)) }; } } @@ -413,7 +352,6 @@ template [[nodiscard]] constexpr Token next_token(s constexpr auto is_eol = [](auto ch) { return ch == chars::ch('\n') || ch == chars::ch('\r'); }; constexpr auto is_whitespace = [=](auto ch) { return ch == chars::ch(' ') || ch == chars::ch('\t') || is_eol(ch); }; - constexpr auto consume = [=](auto ws_input, auto predicate) { auto begin = ws_input.begin(); while (begin != ws_input.end() && predicate(*begin)) { ++begin; } @@ -487,14 +425,14 @@ template struct IndexedList [[nodiscard]] constexpr auto front() const noexcept { return start; } [[nodiscard]] constexpr size_type operator[](size_type index) const noexcept { return start + index; } [[nodiscard]] constexpr size_type back() const noexcept { return static_cast(start + size - 1); } - [[nodiscard]] constexpr auto sublist(const size_type from, - const size_type distance = std::numeric_limits::max()) const noexcept + [[nodiscard]] constexpr auto sublist(const size_type from) const noexcept { - if (distance == std::numeric_limits::max()) { - return IndexedList{ static_cast(start + from), static_cast(size - from) }; - } else { - return IndexedList{ static_cast(start + from), distance }; - }; + return IndexedList{ static_cast(start + from), static_cast(size - from) }; + } + + [[nodiscard]] constexpr auto sublist(const size_type from, const size_type distance) const noexcept + { + return IndexedList{ static_cast(start + from), distance }; } }; @@ -517,7 +455,7 @@ template struct Identifier { using size_type = SizeType; IndexedString value; - [[nodiscard]] constexpr auto substr(const size_type from) const noexcept { return Identifier{ value.substr(from) }; } + [[nodiscard]] constexpr auto substr(const size_type from) const { return Identifier{ value.substr(from) }; } [[nodiscard]] constexpr bool operator==(const Identifier &) const noexcept = default; }; @@ -556,45 +494,46 @@ struct cons_expr using literal_list_type = LiteralList; using error_type = Error; + template using stack_vector = SmallVector>; + struct SExpr; struct Closure; - template [[nodiscard]] static consteval auto str(char const (&input)[Size]) noexcept + template + [[nodiscard]] static consteval auto str(char const (&input)[Size]) noexcept// NOLINT(modernize-avoid-c-arrays) { return chars::str(input); } - - template static constexpr bool visit_helper(SExpr &result, auto Func, auto &variant) + template + [[nodiscard]] static constexpr bool visit_helper(SExpr &result, auto Func, auto &variant) noexcept { - auto *value = std::get_if(&variant); - if (value != nullptr) { + if (auto *value = std::get_if(&variant); value != nullptr) { result = Func(*value); return true; - } else { - return false; } + return false; } - template static constexpr SExpr visit(auto visitor, const std::variant &value) + template static constexpr SExpr visit(auto visitor, const std::variant &value) noexcept { SExpr result{}; + // || will make this short circuit and stop on first matching function ((visit_helper(result, visitor, value) || ...)); return result; } - using LexicalScope = SmallOptimizedVector, BuiltInSymbolsSize, list_type>; + using LexicalScope = SmallVector, BuiltInSymbolsSize, list_type>; using function_ptr = SExpr (*)(cons_expr &, LexicalScope &, list_type); using Atom = std::variant; - struct FunctionPtr { - enum struct Type { other, do_expr, let_expr, lambda_expr, define_expr }; + enum struct Type : std::uint8_t { other, do_expr, let_expr, lambda_expr, define_expr }; function_ptr ptr{ nullptr }; Type type{ Type::other }; - [[nodiscard]] constexpr bool operator==(const FunctionPtr &other) const noexcept + [[nodiscard]] constexpr bool operator==(const FunctionPtr &other) const { // this pointer comparison is giving me a problem in constexpr context // it feels like a bug in GCC, but not sure @@ -620,9 +559,9 @@ struct cons_expr }; static_assert(std::is_trivially_copyable_v && std::is_trivially_destructible_v, - "cons_expr does not work well with non-trivial types"); + "cons_expr does not work with non-trivial types"); - template [[nodiscard]] constexpr const Result *get_if(const SExpr *sexpr) const + template [[nodiscard]] constexpr const Result *get_if(const SExpr *sexpr) const noexcept { if (sexpr == nullptr) { return nullptr; } @@ -638,8 +577,51 @@ struct cons_expr } LexicalScope global_scope{}; - SmallOptimizedVector strings{}; - SmallOptimizedVector values{}; + SmallVector strings{}; + SmallVector values{}; + + SmallVector> object_scratch{}; + SmallVector, 32, IndexedList> variables_scratch{}; + SmallVector> string_scratch{}; + + template struct Scratch + { + constexpr explicit Scratch(ScratchTo &t_data) noexcept : data(&t_data) {} + constexpr explicit Scratch(ScratchTo &t_data, auto initial_values) noexcept : Scratch(t_data) + { + for (const auto &obj : initial_values) { push_back(obj); } + } + Scratch(const Scratch &) = delete; + constexpr Scratch(Scratch &&other) noexcept + : data{ std::exchange(other.data, nullptr) }, initial_size{ other.initial_size }, + current_size{ other.current_size } + {} + auto &operator=(Scratch &&) = delete; + auto &operator=(const Scratch &&) = delete; + constexpr ~Scratch() noexcept + { + if (data != nullptr) { data->resize(initial_size); } + } + [[nodiscard]] constexpr auto begin() const noexcept { return std::next(data->begin(), initial_size); } + [[nodiscard]] constexpr auto end() const noexcept { return std::next(data->begin(), current_size); } + constexpr void emplace_back(auto &&...param) noexcept + { + assert(data->size() == current_size); + data->emplace_back(param...); + current_size = data->size(); + } + constexpr void push_back(auto obj) noexcept + { + assert(data->size() == current_size); + data->push_back(obj); + current_size = data->size(); + } + + private: + ScratchTo *data; + size_type initial_size = data->size(); + size_type current_size = data->size(); + }; struct Closure @@ -647,7 +629,7 @@ struct cons_expr list_type parameter_names; list_type statements; - [[nodiscard]] constexpr bool operator==(const Closure &) const noexcept = default; + [[nodiscard]] constexpr bool operator==(const Closure &) const = default; [[nodiscard]] constexpr SExpr invoke(cons_expr &engine, LexicalScope &scope, list_type params) const { @@ -664,18 +646,19 @@ struct cons_expr new_scope.emplace_back(engine.get_if(&name)->value, engine.eval(scope, parameter)); } - std::vector fixed_statements; + Scratch fixed_statements{ engine.object_scratch }; for (const auto &statement : engine.values[statements]) { fixed_statements.push_back(engine.fix_identifiers(statement, {}, new_scope)); } + // TODO set up tail call elimination for last element of the sequence being evaluated? return engine.sequence(new_scope, engine.values.insert_or_find(fixed_statements)); } }; - [[nodiscard]] constexpr std::pair> parse(string_view_type input) noexcept + [[nodiscard]] constexpr std::pair> parse(string_view_type input) { - std::vector retval; + Scratch retval{ object_scratch }; auto token = next_token(input); @@ -722,7 +705,7 @@ struct cons_expr } // Guaranteed to be initialized at compile time - consteval cons_expr() + consteval cons_expr() noexcept { add(str("+"), SExpr{ FunctionPtr{ binary_left_fold, FunctionPtr::Type::other } }); add(str("*"), SExpr{ FunctionPtr{ binary_left_fold, FunctionPtr::Type::other } }); @@ -772,20 +755,35 @@ struct cons_expr return make_error(str("Function"), function); } - template - [[nodiscard]] constexpr static function_ptr make_evaluator() noexcept + + template [[nodiscard]] constexpr static function_ptr make_evaluator() { return function_ptr{ [](cons_expr &engine, LexicalScope &scope, list_type params) -> SExpr { if (params.size != sizeof...(Param)) { return engine.make_error(str("wrong param count for function"), params); } - // these invoke UB if the type is not contained, we need to come up with a better solution auto impl = [&](std::integer_sequence) { + std::tuple evaled_params{ engine.eval_to>(scope, engine.values[params[Idx]])... }; + + SExpr error; + + // See if any parameter evaluations errored + const bool errored = !([&] { + if (std::get(evaled_params).has_value()) { + return true; + } else { + error = std::get(evaled_params).error(); + return false; + } + }() && ...); + + if (errored) { return engine.make_error(str("parameter type mismatch"), error); } + + // types have already been verified, so I can just `*` the expected safely to avoid exception checks if constexpr (std::is_same_v) { - std::invoke(Func, engine.eval_to>(scope, engine.values[params[Idx]]).value()...); + std::invoke(Func, *std::get(evaled_params)...); return SExpr{ Atom{ std::monostate{} } }; } else { - return SExpr{ std::invoke( - Func, engine.eval_to>(scope, engine.values[params[Idx]]).value()...) }; + return SExpr{ std::invoke(Func, *std::get(evaled_params)...) }; } }; @@ -794,24 +792,24 @@ struct cons_expr } template - [[nodiscard]] constexpr static function_ptr make_evaluator(Ret (*)(Param...)) noexcept + [[maybe_unused]] [[nodiscard]] constexpr static function_ptr make_evaluator(Ret (*)(Param...)) { return make_evaluator(); } template - [[nodiscard]] constexpr static function_ptr make_evaluator(Ret (Type::*)(Param...) const) noexcept + [[nodiscard]] constexpr static function_ptr make_evaluator(Ret (Type::*)(Param...) const) { return make_evaluator(); } template - [[nodiscard]] constexpr static function_ptr make_evaluator(Ret (Type::*)(Param...)) noexcept + [[maybe_unused]] [[nodiscard]] constexpr static function_ptr make_evaluator(Ret (Type::*)(Param...)) { return make_evaluator(); } - template [[nodiscard]] constexpr static function_ptr make_evaluator() noexcept + template [[nodiscard]] constexpr static function_ptr make_evaluator() { return make_evaluator(Func); } @@ -854,7 +852,7 @@ struct cons_expr } template - [[nodiscard]] constexpr std::expected eval_to(LexicalScope &scope, const SExpr expr) noexcept + [[nodiscard]] constexpr std::expected eval_to(LexicalScope &scope, const SExpr expr) { if constexpr (std::is_same_v) { return eval(scope, expr); @@ -862,10 +860,14 @@ struct cons_expr if (const auto *obj = std::get_if(&expr.value); obj != nullptr) { return *obj; } } else { if (const auto *atom = std::get_if(&expr.value); atom != nullptr) { - if (const auto *value = std::get_if(atom); value != nullptr) { - return *value; - } else if (!std::holds_alternative(*atom)) { - return std::unexpected(expr); + if constexpr (std::is_same_v) { + if (const auto *value = std::get_if(atom); value != nullptr) { return strings.view(*value); } + } else { + if (const auto *value = std::get_if(atom); value != nullptr) { + return *value; + } else if (!std::holds_alternative(*atom)) { + return std::unexpected(expr); + } } } } @@ -875,19 +877,15 @@ struct cons_expr [[nodiscard]] static constexpr SExpr list(cons_expr &engine, LexicalScope &scope, list_type params) { - std::vector result; - result.reserve(params.size); - + Scratch result{ engine.object_scratch }; for (const auto ¶m : engine.values[params]) { result.push_back(engine.eval(scope, param)); } - return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; } - constexpr std::vector get_lambda_parameter_names(const SExpr &sexpr) + constexpr auto get_lambda_parameter_names(const SExpr &sexpr) { - std::vector retval; + Scratch retval{ string_scratch }; if (auto *parameter_list = get_if(&sexpr); parameter_list != nullptr) { - retval.reserve(parameter_list->size); for (const auto &expr : values[*parameter_list]) { if (auto *local_id = get_if(&expr); local_id != nullptr) { retval.push_back(local_id->value); } } @@ -902,8 +900,8 @@ struct cons_expr auto locals = engine.get_lambda_parameter_names(engine.values[params[0]]); // replace all references to captured values with constant copies - std::vector fixed_statements; - fixed_statements.reserve(params.size); + Scratch fixed_statements{ engine.object_scratch }; + for (const auto &statement : engine.values[params.sublist(1)]) { // all of current scope is const and capturable fixed_statements.push_back(engine.fix_identifiers(statement, locals, scope)); @@ -928,14 +926,14 @@ struct cons_expr return *items; } - [[nodiscard]] constexpr std::expected get_list_range(SExpr expr, + [[nodiscard]] constexpr std::expected get_list_range(SExpr expr, string_view_type message, size_type min = 0, size_type max = std::numeric_limits::max()) { auto list = get_list(expr, message, min, max); if (!list) { return std::unexpected(list.error()); } - return values[list.value()]; + return values[*list]; } [[nodiscard]] constexpr SExpr fix_do_identifiers(list_type list, @@ -943,9 +941,8 @@ struct cons_expr std::span local_identifiers, const LexicalScope &local_constants) { - std::vector new_locals{ local_identifiers.begin(), local_identifiers.end() }; - - std::vector new_params; + Scratch new_locals{ string_scratch, local_identifiers }; + Scratch new_params{ object_scratch }; // collect all locals const auto params = get_list(values[first_index + 1], str("malformed do expression")); @@ -964,17 +961,19 @@ struct cons_expr const auto param_list = get_list(param, str("malformed do expression"), 2); if (!param_list) { return params.error(); } - std::vector new_param; - new_param.push_back(values[(*param_list)[0]]); - new_param.push_back(fix_identifiers(values[(*param_list)[1]], local_identifiers, local_constants)); + std::array new_param{ values[(*param_list)[0]], + fix_identifiers(values[(*param_list)[1]], local_identifiers, local_constants) }; + // increment thingy (optional) if (param_list->size == 3) { - new_param.push_back(fix_identifiers(values[(*param_list)[2]], new_locals, local_constants)); + new_param[2] = (fix_identifiers(values[(*param_list)[2]], new_locals, local_constants)); } - new_params.push_back(SExpr{ values.insert_or_find(new_param) }); + new_params.push_back( + SExpr{ values.insert_or_find(std::span{ new_param.begin(), param_list->size == 3u ? 3u : 2u }) }); } - std::vector new_do; + Scratch new_do{ object_scratch }; + // fixup pointer to "do" function new_do.push_back(fix_identifiers(values[first_index], new_locals, local_constants)); @@ -993,19 +992,19 @@ struct cons_expr std::span local_identifiers, const LexicalScope &local_constants) { - std::vector new_locals{ local_identifiers.begin(), local_identifiers.end() }; + Scratch new_locals{ string_scratch, local_identifiers }; - std::vector new_params; + Scratch new_params{ object_scratch }; const auto params = get_list_range(values[static_cast(first_index + 1)], str("malformed let expression")); if (!params) { return params.error(); } - for (const auto ¶m : params.value()) { + for (const auto ¶m : *params) { const auto param_list = get_list(param, str("malformed let expression"), 2, 2); if (!param_list) { return param_list.error(); } - auto id = get_if(&values[(*param_list)[0]]); + auto *id = get_if(&values[(*param_list)[0]]); if (id == nullptr) { return make_error(str("malformed let expression"), list); } new_locals.push_back(id->value); @@ -1015,8 +1014,10 @@ struct cons_expr new_params.push_back(SExpr{ values.insert_or_find(new_param) }); } - std::vector new_let{ fix_identifiers(values[first_index], new_locals, local_constants), - SExpr{ values.insert_or_find(new_params) } }; + Scratch new_let{ object_scratch }; + + new_let.push_back(fix_identifiers(values[first_index], new_locals, local_constants)); + new_let.push_back(SExpr{ values.insert_or_find(new_params) }); for (size_type index = first_index + 2; index < list.size + list.start; ++index) { new_let.push_back(fix_identifiers(values[index], new_locals, local_constants)); @@ -1029,7 +1030,7 @@ struct cons_expr std::span local_identifiers, const LexicalScope &local_constants) { - std::vector new_locals{ local_identifiers.begin(), local_identifiers.end() }; + Scratch new_locals{ string_scratch, local_identifiers }; const auto *id = get_if(&values[static_cast(first_index + 1)]); @@ -1048,12 +1049,12 @@ struct cons_expr std::span local_identifiers, const LexicalScope &local_constants) { - std::vector new_locals{ local_identifiers.begin(), local_identifiers.end() }; auto lambda_locals = get_lambda_parameter_names(values[first_index + 1]); - new_locals.insert(new_locals.end(), lambda_locals.begin(), lambda_locals.end()); + Scratch new_locals{ string_scratch, local_identifiers }; + for (const auto &value : lambda_locals) { new_locals.push_back(value); } - std::vector new_lambda{ fix_identifiers(values[first_index], new_locals, local_constants), - values[first_index + 1] }; + Scratch new_lambda{ object_scratch, + std::array{ fix_identifiers(values[first_index], new_locals, local_constants), values[first_index + 1] } }; for (size_type index = first_index + 2; index < list.size + list.start; ++index) { new_lambda.push_back(fix_identifiers(values[index], new_locals, local_constants)); @@ -1084,8 +1085,9 @@ struct cons_expr return fix_do_identifiers(*list, first_index, local_identifiers, local_constants); } } - std::vector result; - result.reserve(list->size); + + + Scratch result{ object_scratch }; for (const auto &value : values[*list]) { result.push_back(fix_identifiers(value, local_identifiers, local_constants)); } @@ -1106,31 +1108,24 @@ struct cons_expr return input; } - [[nodiscard]] constexpr SExpr make_error(string_view_type description, list_type context) noexcept + [[nodiscard]] constexpr SExpr make_error(string_view_type description, list_type context) { return SExpr{ Error{ strings.insert_or_find(description), context } }; } - [[nodiscard]] constexpr SExpr make_error(string_view_type description, SExpr value) noexcept + [[nodiscard]] constexpr SExpr make_error(string_view_type description, auto... value) { - return make_error(description, values.insert_or_find(std::array{ value })); - } - - [[nodiscard]] constexpr SExpr make_error(string_view_type description, SExpr value, SExpr value2) noexcept - { - return make_error(description, values.insert_or_find(std::array{ value, value2 })); + return make_error(description, values.insert_or_find(std::array{ SExpr{ value }... })); } // // built-ins // - [[nodiscard]] static constexpr SExpr letter(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr letter(cons_expr &engine, LexicalScope &scope, list_type params) { if (params.empty()) { return engine.make_error(str("(let ((var1 val1) ...) [expr...])"), params); } - std::vector> variables; - auto new_scope = scope; const auto variable_list = engine.get_list_range(engine.values[params[0]], str("((var1 val1) ...)")); @@ -1150,15 +1145,14 @@ struct cons_expr } - [[nodiscard]] static constexpr SExpr doer(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr doer(cons_expr &engine, LexicalScope &scope, list_type params) { if (params.size < 2) { return engine.make_error( str("(do ((var1 val1 [iter_expr1]) ...) (terminate_condition [result...]) [body...])"), params); } - std::vector> variables; - std::vector variable_names; + Scratch variables{ engine.variables_scratch }; auto *variable_list = engine.get_if(&engine.values[params[0]]); @@ -1188,9 +1182,8 @@ struct cons_expr if (variable_parts->size == 3) { variables.emplace_back(index, variable_parts_list[2]); } } - for (auto &variable : variables) { - variable.second = engine.fix_identifiers(variable.second, variable_names, scope); - } + Scratch variable_names{ engine.string_scratch }; + for (auto &[index, value] : variables) { value = engine.fix_identifiers(value, variable_names, scope); } for (const auto &local : new_scope) { variable_names.push_back(local.first); } @@ -1201,9 +1194,6 @@ struct cons_expr } const auto terminators = engine.values[*terminator_list]; - // reuse the storage created for the new values on each iteration - std::vector> new_values; - auto fixed_up_terminator = engine.fix_identifiers(terminators[0], variable_names, scope); // continue while terminator test is false @@ -1217,23 +1207,23 @@ struct cons_expr // evaluate body [[maybe_unused]] const auto result = engine.sequence(new_scope, params.sublist(2)); + Scratch new_values{ engine.variables_scratch }; + // iterate loop variables for (const auto &[index, expr] : variables) { new_values.emplace_back(index, engine.eval(new_scope, expr)); } // update values for (auto &[index, value] : new_values) { new_scope[index].second = value; } - - new_values.clear(); } } // evaluate sequence of termination expressions - return engine.sequence(new_scope, terminators.span.sublist(1)); + return engine.sequence(new_scope, terminator_list->sublist(1)); } template [[nodiscard]] constexpr std::expected - eval_to(LexicalScope &scope, list_type params, string_view_type expected) noexcept + eval_to(LexicalScope &scope, list_type params, string_view_type expected) { if (params.size != 1) { return std::unexpected(make_error(expected, params)); } auto first = eval_to(scope, values[params[0]]); @@ -1244,7 +1234,7 @@ struct cons_expr template [[nodiscard]] constexpr std::expected, SExpr> - eval_to(LexicalScope &scope, list_type params, string_view_type expected) noexcept + eval_to(LexicalScope &scope, list_type params, string_view_type expected) { if (params.size != 2) { return std::unexpected(make_error(expected, params)); } @@ -1256,14 +1246,14 @@ struct cons_expr return std::tuple{ *first, *second }; } - [[nodiscard]] static constexpr SExpr append(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr append(cons_expr &engine, LexicalScope &scope, list_type params) { auto evaled_params = engine.eval_to(scope, params, str("(append LiteralList LiteralList)")); if (!evaled_params) { return evaled_params.error(); } const auto &[first, second] = *evaled_params; - std::vector result; + Scratch result{ engine.object_scratch }; for (const auto &value : engine.values[first.items]) { result.push_back(value); } for (const auto &value : engine.values[second.items]) { result.push_back(value); } @@ -1271,13 +1261,13 @@ struct cons_expr return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; } - [[nodiscard]] static constexpr SExpr cons(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr cons(cons_expr &engine, LexicalScope &scope, list_type params) { auto evaled_params = engine.eval_to(scope, params, str("(cons Expr LiteralList)")); if (!evaled_params) { return evaled_params.error(); } const auto &[front, list] = *evaled_params; - std::vector result; + Scratch result{ engine.object_scratch }; if (const auto *list_front = std::get_if(&front.value); list_front != nullptr) { result.push_back(SExpr{ list_front->items }); @@ -1289,6 +1279,7 @@ struct cons_expr return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; } + template [[nodiscard]] static constexpr SExpr error_or_else(const std::expected &obj, auto callable) { @@ -1299,20 +1290,19 @@ struct cons_expr } } - - [[nodiscard]] static constexpr SExpr cdr(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr cdr(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(cdr Non-Empty-LiteralList)")), [&](const auto &list) { return SExpr{ list.sublist(1) }; }); } - [[nodiscard]] static constexpr SExpr car(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr car(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(car Non-Empty-LiteralList)")), [&](const auto &list) { return engine.values[list.front()]; }); } - [[nodiscard]] static constexpr SExpr applier(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr applier(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(apply Function LiteralList)")), [&](const auto &evaled_params) { @@ -1320,13 +1310,13 @@ struct cons_expr }); } - [[nodiscard]] static constexpr SExpr evaler(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr evaler(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(eval LiteralList)")), [&](const auto &list) { return engine.eval(engine.global_scope, SExpr{ list.items }); }); } - [[nodiscard]] static constexpr SExpr ifer(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr ifer(cons_expr &engine, LexicalScope &scope, list_type params) { // need to be careful to not execute unexecuted branches if (params.size != 3) { return engine.make_error(str("(if bool-cond then else)"), params); } @@ -1355,7 +1345,7 @@ struct cons_expr return SExpr{ Atom{ std::monostate{} } }; } - [[nodiscard]] static constexpr SExpr definer(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr definer(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(define Identifier Expression)")), [&](const auto &evaled) { @@ -1367,7 +1357,7 @@ struct cons_expr // take a string_view and return a C++ function object // of unspecified type. template - [[nodiscard]] constexpr auto make_callable(string_view_type function) noexcept + [[nodiscard]] constexpr auto make_callable(string_view_type function) requires std::is_function_v { auto impl = [this, function](Ret (*)(Params...)) { @@ -1390,8 +1380,7 @@ struct cons_expr } template - [[nodiscard]] static constexpr SExpr - binary_left_fold(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr binary_left_fold(cons_expr &engine, LexicalScope &scope, list_type params) { auto fold = [&engine, &scope, params](Param first) -> SExpr { if constexpr (requires(Param p1, Param p2) { Op(p1, p2); }) { @@ -1419,21 +1408,21 @@ struct cons_expr return engine.make_error(str("operator requires at east two parameters"), params); } - [[nodiscard]] static constexpr SExpr logical_and(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr logical_and(cons_expr &engine, LexicalScope &scope, list_type params) { for (const auto &next : engine.values[params] | engine.eval_transform(scope)) { if (!next) { return engine.make_error(str("parameter not boolean"), next.error()); } - if (!next.value()) { return SExpr{ Atom{ false } }; } + if (!(*next)) { return SExpr{ Atom{ false } }; } } return SExpr{ Atom{ true } }; } - [[nodiscard]] static constexpr SExpr logical_or(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + [[nodiscard]] static constexpr SExpr logical_or(cons_expr &engine, LexicalScope &scope, list_type params) { for (const auto &next : engine.values[params] | engine.eval_transform(scope)) { if (!next) { return engine.make_error(str("parameter not boolean"), next.error()); } - if (next.value()) { return SExpr{ Atom{ true } }; } + if (*next) { return SExpr{ Atom{ true } }; } } return SExpr{ Atom{ false } }; @@ -1441,7 +1430,7 @@ struct cons_expr template [[nodiscard]] static constexpr SExpr - binary_boolean_apply_pairwise(cons_expr &engine, LexicalScope &scope, list_type params) noexcept + binary_boolean_apply_pairwise(cons_expr &engine, LexicalScope &scope, list_type params) { auto sum = [&engine, &scope, params](Param next) -> SExpr { if constexpr (requires(Param p1, Param p2) { Op(p1, p2); }) { @@ -1468,6 +1457,20 @@ struct cons_expr return engine.make_error(str("supported types"), params); } + + [[nodiscard]] constexpr SExpr evaluate(string_view_type input) + { + const auto result = parse(input).first; + const auto *list = std::get_if(&result.value); + + if (list != nullptr) { return sequence(global_scope, *list); } + return result; + } + + template [[nodiscard]] constexpr std::expected evaluate_to(string_view_type input) + { + return eval_to(global_scope, evaluate(input)); + } }; diff --git a/src/ccons_expr/main.cpp b/src/ccons_expr/main.cpp index 6096c98..170b8b2 100644 --- a/src/ccons_expr/main.cpp +++ b/src/ccons_expr/main.cpp @@ -97,10 +97,17 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) sizeof(lefticus::cons_expr<>), sizeof(lefticus::cons_expr<>::SExpr), sizeof(lefticus::cons_expr<>::Atom))), - ftxui::text(std::format("string used: {} symbols used: {} values used: {}", + ftxui::text(std::format("string used: {}/{} symbols used: {}/{} values used: {}/{}", evaluator.strings.small_size_used, + evaluator.strings.small_capacity, evaluator.global_scope.small_size_used, - evaluator.values.small_size_used)), + evaluator.global_scope.small_capacity, + evaluator.values.small_size_used, + evaluator.values.small_capacity)), + ftxui::text(std::format("string_scratch used: {} object_scratch used: {} variables_scratch used: {}", + evaluator.string_scratch.small_size_used, + evaluator.object_scratch.small_size_used, + evaluator.variables_scratch.small_size_used)), ftxui::text(std::format( "GIT SHA: {} version string: {}", cons_expr::cmake::git_sha, cons_expr::cmake::project_version)) }); }; diff --git a/src/cons_expr_cli/main_minimal.cpp b/src/cons_expr_cli/main_minimal.cpp index 6a83546..c773b5c 100644 --- a/src/cons_expr_cli/main_minimal.cpp +++ b/src/cons_expr_cli/main_minimal.cpp @@ -1,36 +1,17 @@ #include -template> struct null_container -{ - constexpr const Contained *begin() const { return &dummyobj; } - - constexpr const Contained *end() const { return &dummyobj; } - - constexpr const Contained &operator[](const std::size_t) const { return dummyobj; } - constexpr Contained &operator[](const std::size_t) { return dummyobj; } +#include - constexpr void push_back(const Contained &) {} - - Contained dummyobj; - - constexpr bool empty() const { return true; } - constexpr std::size_t size() const { return 0; } -}; -int main(int argc, const char **argv) +constexpr auto evaluate(std::string_view input) { - // lefticus::cons_expr evaluator1; - - // lefticus::cons_expr> - // evaluator; lefticus::cons_expr> evaluator; lefticus::cons_expr> evaluator; - // lefticus::cons_expr> - // evaluator; - lefticus::cons_expr evaluator; - evaluator.sequence( - evaluator.global_scope, std::get::list_type>(evaluator.parse(argv[1]).first.value)); + const auto result = evaluator.parse(input).first.value; + + return evaluator.sequence(evaluator.global_scope, *std::get_if::list_type>(&result)); } + + +int main(int argc, const char **argv) { evaluate(argv[0]); } diff --git a/test/constexpr_tests.cpp b/test/constexpr_tests.cpp index 2ed4f06..a5766bb 100644 --- a/test/constexpr_tests.cpp +++ b/test/constexpr_tests.cpp @@ -12,28 +12,21 @@ static_assert(lefticus::is_cons_expr_v>); static_assert(std::is_trivially_copyable_v::SExpr>); -// we'll be exactly 16k, because we can -// static_assert(sizeof(lefticus::cons_expr<>) == 16384); - -constexpr auto evaluate(std::string_view input) +template constexpr Result evaluate_to(std::string_view input) { lefticus::cons_expr evaluator; - - return evaluator.sequence( - evaluator.global_scope, std::get::list_type>(evaluator.parse(input).first.value)); + return evaluator.evaluate_to(input).value(); } -template constexpr Result evaluate_to(std::string_view input) +// this version exists so we can evaluate an objects +// whose lifetime would have otherwise ended +template constexpr bool evaluate_expected(std::string_view input, auto result) { - if constexpr (std::is_same_v::error_type>) { - return std::get::error_type>(evaluate(input).value); - } else { - return std::get(std::get::Atom>(evaluate(input).value)); - } + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; } - TEST_CASE("Operator identifiers", "[operators]") { STATIC_CHECK(evaluate_to("((if false + *) 3 4)") == 12); @@ -53,6 +46,11 @@ TEST_CASE("basic string_view operators", "[operators]") STATIC_CHECK(evaluate_to(R"((== "hello" "hello"))") == true); } +TEST_CASE("access as string_view", "[strings]") +{ + STATIC_CHECK(evaluate_expected(R"("hello")", "hello")); +} + TEST_CASE("basic integer operators", "[operators]") { STATIC_CHECK(evaluate_to("(+ 1 2)") == 3); @@ -84,6 +82,9 @@ TEST_CASE("basic logical boolean operations", "[operators]") { STATIC_CHECK(evaluate_to("(and true true false)") == false); STATIC_CHECK(evaluate_to("(or false true false true)") == true); + STATIC_CHECK(evaluate_to("(not false)") == true); + STATIC_CHECK(evaluate_to("(not true)") == false); + STATIC_CHECK(evaluate_to("(not false)") == true); } TEST_CASE("basic lambda usage", "[lambdas]") @@ -298,4 +299,4 @@ TEST_CASE("scoped do expression", "[builtins]") TEST_CASE("basic for-each usage", "[builtins]") { // STATIC_CHECK_NOTHROW(evaluate_to("(for-each display '(1 2 3 4))")); -} \ No newline at end of file +}