// Copyright 2021 Benjamin Barenblat // // 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 // // https://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 #include #include #include "goldfishterm/internal/string_capability.h" #include "third_party/abseil/absl/container/flat_hash_map.h" #include "third_party/abseil/absl/strings/match.h" #include "third_party/abseil/absl/strings/numbers.h" #include "third_party/abseil/absl/strings/str_cat.h" #include "third_party/abseil/absl/strings/str_format.h" #include "third_party/abseil/absl/strings/string_view.h" #include "third_party/abseil/absl/time/time.h" #include "third_party/abseil/absl/types/variant.h" namespace goldfishterm_internal { namespace { absl::string_view Slice(const char* start, const char* end) noexcept { return absl::string_view(start, end - start); } // The string capability interpreter, implemented as an re2c-generated DFA. class Interpreter final { public: explicit Interpreter(InterpretStringCapabilityInput input) noexcept : input_(std::move(input)), chars_per_ms_(input_.baud / 1000.0 / (8 * sizeof(char) + input_.extra_bits_per_character)) { result_.cost = 0; } void Run() { enum ConditionalEvaluationState { kNormal, kEvaluatingThen, kSkippingThen, kSkippingElse, } if_state = kNormal; const char* YYCURSOR = input_.capability.c_str(); const char* YYMARKER; const char* a; const char* b; /*!stags:re2c format = "const char* @@;"; */ loop: /*!re2c re2c:define:YYCTYPE = char; re2c:flags:tags = 1; re2c:yyfill:enable = 0; integer = [0-9]+; decimal = integer ("." integer)?; [\x00] { goto done; } "$<" @a ( "0"* [1-9] "0"* ("." "0"*)? | "0"* "." "0"* [1-9] "0"* ) @b "*"? "/"? ">" { if (if_state == kSkippingThen || if_state == kSkippingElse || input_.baud < input_.padding_baud_rate) { goto loop; } float delay_ms; if (!absl::SimpleAtof(Slice(a, b), &delay_ms)) { throw std::logic_error( "goldfishterm: parser produced an invalid float"); } bool per_line_pad = *b == '*'; bool padding_mandatory = *(YYCURSOR - 2) == '/'; if (per_line_pad) { delay_ms *= input_.lines_affected; } if (input_.has_xon_xoff && !padding_mandatory) { // Record that we expect a delay, but don't actually insert a delay. ExpectDelay(absl::Milliseconds(delay_ms)); } else if (input_.pad_char.has_value()) { Bytes(std::string(ceilf(delay_ms * chars_per_ms_), *input_.pad_char)); } else { Delay(absl::Milliseconds(delay_ms)); } goto loop; } "%%" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; Bytes("%"); goto loop; } "%" @a ((":"? [-+# ]+)? decimal? [doxX] | ":-"? integer? "c") { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; EmitPopped(Slice(a, YYCURSOR)); goto loop; } "%" @a ":-"? decimal? "s" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; EmitPopped(Slice(a, YYCURSOR)); goto loop; } "%p" [1-9] { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; int index = *(YYCURSOR - 1) - '0'; try { stack_.push_back(input_.parameters.at(index - 1)); } catch (const std::out_of_range& e) { throw std::runtime_error( absl::StrCat(absl::StrCat("invalid parameter ", index))); } goto loop; } "%P" [A-Za-z] { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; RequireNonempty(); environment_[*(YYCURSOR - 1)] = std::move(stack_.back()); stack_.pop_back(); goto loop; } "%g" [A-Za-z] { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; char var = *(YYCURSOR - 1); auto it = environment_.find(var); if (it == environment_.end()) { throw std::runtime_error(absl::StrCat("undefined variable ", Slice(YYCURSOR - 1, YYCURSOR))); } stack_.push_back(it->second); goto loop; } "%'" [^\x00] "'" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; stack_.push_back(*(YYCURSOR - 2)); goto loop; } "%{" @a integer "}" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; int value; if (!absl::SimpleAtoi(Slice(a, YYCURSOR - 1), &value)) { throw std::logic_error( "goldfishterm: parser produced an invalid int"); } stack_.push_back(value); goto loop; } "%l" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; Unop( [](const std::string& s) { return static_cast(s.size()); }); goto loop; } "%+" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::plus()); goto loop; } "%-" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::minus()); goto loop; } "%*" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::multiplies()); goto loop; } "%/" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::divides()); goto loop; } "%m" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::modulus()); goto loop; } "%&" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::bit_and()); goto loop; } "%|" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::bit_or()); goto loop; } "%^" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::bit_xor()); goto loop; } "%=" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::equal_to()); goto loop; } "%>" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::greater()); goto loop; } "%<" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::less()); goto loop; } "%A" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::logical_and()); goto loop; } "%O" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; BinopN(std::logical_or()); goto loop; } "%!" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; UnopN(std::logical_not()); goto loop; } "%~" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; UnopN(std::bit_not()); goto loop; } "%i" { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; if (input_.parameters.size() < 2) { throw std::runtime_error("%i requires at least two parameters"); } auto* one = absl::get_if(&input_.parameters[0]); auto* two = absl::get_if(&input_.parameters[1]); RequireType(one); RequireType(two); ++*one; ++*two; goto loop; } "%?" { if (if_state != kNormal) { throw std::logic_error("goldfishterm: unexpected %?"); } goto loop; } "%t" { if (if_state == kNormal) { RequireNonempty(); int* condition_int = absl::get_if(&stack_.back()); // Strings are true, as are nonzero integers. bool condition = condition_int == nullptr || *condition_int; stack_.pop_back(); if_state = condition ? kEvaluatingThen : kSkippingThen; } else if (if_state != kSkippingElse) { throw std::logic_error("goldfishterm: unexpected %t"); } goto loop; } "%e" { switch (if_state) { case kNormal: throw std::logic_error("goldfishterm: unexpected %e"); break; case kEvaluatingThen: if_state = kSkippingElse; break; case kSkippingThen: if_state = kNormal; break; case kSkippingElse: break; } goto loop; } "%;" { if_state = kNormal; goto loop; } * { if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop; Bytes(Slice(YYCURSOR - 1, YYCURSOR)); goto loop; } */ done: Flush(); } const InterpretStringCapabilityResult& result() noexcept { return result_; } private: void RequireNonempty() { if (stack_.empty()) { throw std::runtime_error("stack underflow"); } } // A companion to absl::get_if: Requires that the passed pointer is nonnull // and throws an appropriate error if it's not. template void RequireType(const void* v) { if (v == nullptr) { if constexpr (std::is_same_v) { throw std::runtime_error("type error: expected integer"); } else if constexpr (std::is_same_v) { throw std::runtime_error("type error: expected string"); } else { throw std::runtime_error("type error"); } } } // Executes push(f(pop())). template void Unop(std::function f) { RequireNonempty(); T* v = absl::get_if(&stack_.back()); RequireType(v); stack_.back() = f(*v); } // Executes x = pop(); push(f(pop(), x)). template void Binop(std::function f) { if (stack_.size() < 2) { throw std::runtime_error("stack underflow during binary operation"); } int target = stack_.size() - 2; T1* one = absl::get_if(&stack_[target]); T2* two = absl::get_if(&stack_.back()); RequireType(one); RequireType(two); stack_[target] = f(*one, *two); stack_.pop_back(); } // Convenience specializations for Unop and Binop over integers. void UnopN(std::function f) { Unop(f); } void BinopN(std::function f) { Binop(f); } template void EmitPopped(absl::string_view format) { if (format[0] == ':') { // The colon is used by to disambiguate parsing. It isn't interesting to // absl::FormatUntyped. format = format.substr(1); } std::string out; auto* param = absl::get_if(&stack_.back()); RequireType(param); if (!absl::FormatUntyped(&out, absl::UntypedFormatSpec(absl::StrCat("%", format)), {absl::FormatArg(*param)})) { throw std::logic_error( absl::StrCat("absl::FormatUntyped failed with format ", format)); } Bytes(out); stack_.pop_back(); } void ExpectDelay(absl::Duration delay) noexcept { result_.cost += absl::ToDoubleMilliseconds(delay) * chars_per_ms_; } void Delay(absl::Duration delay) noexcept { if (!pending_bytes_.empty()) { Flush(); } pending_delay_.Increase(delay); ExpectDelay(delay); } void Bytes(absl::string_view bytes) noexcept { pending_bytes_.Append(bytes); result_.cost += bytes.size(); } void Flush() noexcept { if (!pending_delay_.zero()) { result_.terms.push_back( std::make_shared(std::move(pending_delay_))); }; if (!pending_bytes_.empty()) { result_.terms.push_back( std::make_shared(std::move(pending_bytes_))); }; } InterpretStringCapabilityInput input_; double chars_per_ms_; std::vector stack_; absl::flat_hash_map environment_; EmitDelay pending_delay_; EmitBytes pending_bytes_; InterpretStringCapabilityResult result_; }; } // namespace InterpretStringCapabilityResult InterpretStringCapability( const InterpretStringCapabilityInput& input) { Interpreter interp(input); interp.Run(); return interp.result(); } } // namespace goldfishterm_internal