aboutsummaryrefslogtreecommitdiff
path: root/goldfishterm/internal/string_capability.re
diff options
context:
space:
mode:
Diffstat (limited to 'goldfishterm/internal/string_capability.re')
-rw-r--r--goldfishterm/internal/string_capability.re483
1 files changed, 483 insertions, 0 deletions
diff --git a/goldfishterm/internal/string_capability.re b/goldfishterm/internal/string_capability.re
new file mode 100644
index 0000000..6920066
--- /dev/null
+++ b/goldfishterm/internal/string_capability.re
@@ -0,0 +1,483 @@
+// 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 <math.h>
+
+#include <functional>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#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<int>(Slice(a, YYCURSOR));
+ goto loop;
+ }
+
+ "%" @a ":-"? decimal? "s" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ EmitPopped<std::string>(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<std::string, int>(
+ [](const std::string& s) { return static_cast<int>(s.size()); });
+ goto loop;
+ }
+
+ "%+" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::plus<int>());
+ goto loop;
+ }
+
+ "%-" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::minus<int>());
+ goto loop;
+ }
+
+ "%*" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::multiplies<int>());
+ goto loop;
+ }
+
+ "%/" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::divides<int>());
+ goto loop;
+ }
+
+ "%m" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::modulus<int>());
+ goto loop;
+ }
+
+ "%&" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::bit_and<int>());
+ goto loop;
+ }
+
+ "%|" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::bit_or<int>());
+ goto loop;
+ }
+
+ "%^" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::bit_xor<int>());
+ goto loop;
+ }
+
+ "%=" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::equal_to<int>());
+ goto loop;
+ }
+
+ "%>" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::greater<int>());
+ goto loop;
+ }
+
+ "%<" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::less<int>());
+ goto loop;
+ }
+
+ "%A" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::logical_and<int>());
+ goto loop;
+ }
+
+ "%O" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::logical_or<int>());
+ goto loop;
+ }
+
+ "%!" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ UnopN(std::logical_not<int>());
+ goto loop;
+ }
+
+ "%~" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ UnopN(std::bit_not<int>());
+ 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<int>(&input_.parameters[0]);
+ auto* two = absl::get_if<int>(&input_.parameters[1]);
+ RequireType<int>(one);
+ RequireType<int>(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<int>(&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 <typename T>
+ void RequireType(const void* v) {
+ if (v == nullptr) {
+ if constexpr (std::is_same_v<T, const int>) {
+ throw std::runtime_error("type error: expected integer");
+ } else if constexpr (std::is_same_v<T, const std::string>) {
+ throw std::runtime_error("type error: expected string");
+ } else {
+ throw std::runtime_error("type error");
+ }
+ }
+ }
+
+ // Executes push(f(pop())).
+ template <typename T, typename R>
+ void Unop(std::function<R(T)> f) {
+ RequireNonempty();
+ T* v = absl::get_if<T>(&stack_.back());
+ RequireType<int>(v);
+ stack_.back() = f(*v);
+ }
+
+ // Executes x = pop(); push(f(pop(), x)).
+ template <typename T1, typename T2, typename R>
+ void Binop(std::function<R(T1, T2)> f) {
+ if (stack_.size() < 2) {
+ throw std::runtime_error("stack underflow during binary operation");
+ }
+ int target = stack_.size() - 2;
+ T1* one = absl::get_if<T1>(&stack_[target]);
+ T2* two = absl::get_if<T2>(&stack_.back());
+ RequireType<T1>(one);
+ RequireType<T2>(two);
+ stack_[target] = f(*one, *two);
+ stack_.pop_back();
+ }
+
+ // Convenience specializations for Unop and Binop over integers.
+ void UnopN(std::function<int(int)> f) { Unop<int, int>(f); }
+ void BinopN(std::function<int(int, int)> f) { Binop<int, int, int>(f); }
+
+ template <typename T>
+ 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<T>(&stack_.back());
+ RequireType<T>(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<EmitDelay>(std::move(pending_delay_)));
+ };
+ if (!pending_bytes_.empty()) {
+ result_.terms.push_back(
+ std::make_shared<EmitBytes>(std::move(pending_bytes_)));
+ };
+ }
+
+ InterpretStringCapabilityInput input_;
+ double chars_per_ms_;
+
+ std::vector<StringCapabilityParameter> stack_;
+ absl::flat_hash_map<char, StringCapabilityParameter> 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