aboutsummaryrefslogtreecommitdiff
path: root/goldfishterm/internal
diff options
context:
space:
mode:
Diffstat (limited to 'goldfishterm/internal')
-rw-r--r--goldfishterm/internal/emit.cc47
-rw-r--r--goldfishterm/internal/emit.h106
-rw-r--r--goldfishterm/internal/string_capability.h106
-rw-r--r--goldfishterm/internal/string_capability.re483
-rw-r--r--goldfishterm/internal/string_capability_test.cc372
5 files changed, 1114 insertions, 0 deletions
diff --git a/goldfishterm/internal/emit.cc b/goldfishterm/internal/emit.cc
new file mode 100644
index 0000000..2cc7609
--- /dev/null
+++ b/goldfishterm/internal/emit.cc
@@ -0,0 +1,47 @@
+// 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 "goldfishterm/internal/emit.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <memory>
+#include <system_error>
+#include <vector>
+
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/clock.h"
+#include "third_party/abseil/absl/types/span.h"
+
+namespace goldfishterm_internal {
+
+void EmitBytes::Emit(std::ostream& out) const {
+ out.write(bytes_.data(), bytes_.size());
+}
+
+void EmitDelay::Emit(std::ostream& out) const noexcept {
+ out.flush();
+ absl::SleepFor(delay_);
+}
+
+void Emit(absl::Span<const std::shared_ptr<const EmitTerm>> terms,
+ std::ostream& out) {
+ for (const auto& term : terms) {
+ term->Emit(out);
+ }
+}
+
+} // namespace goldfishterm_internal
diff --git a/goldfishterm/internal/emit.h b/goldfishterm/internal/emit.h
new file mode 100644
index 0000000..371f6b0
--- /dev/null
+++ b/goldfishterm/internal/emit.h
@@ -0,0 +1,106 @@
+// 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.
+
+// An extremely simple EDSL for describing output to the terminal. It supports
+// exactly two instructions: "send some bytes to the terminal" and "sleep". The
+// string capability interpreter produces terms in this EDSL, which makes it
+// easier to test.
+
+#ifndef EC_GOLDFISHTERM_INTERNAL_EMIT_H_
+#define EC_GOLDFISHTERM_INTERNAL_EMIT_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/time.h"
+#include "third_party/abseil/absl/types/span.h"
+
+namespace goldfishterm_internal {
+
+class EmitTerm {
+ public:
+ virtual void Emit(std::ostream&) const = 0;
+
+ virtual ~EmitTerm() noexcept = default;
+};
+
+class EmitBytes final : public EmitTerm {
+ public:
+ explicit EmitBytes() noexcept = default;
+
+ explicit EmitBytes(absl::string_view bytes) noexcept : bytes_(bytes) {}
+
+ EmitBytes(const EmitBytes&) = default;
+ EmitBytes& operator=(const EmitBytes&) = default;
+ EmitBytes(EmitBytes&&) noexcept = default;
+ EmitBytes& operator=(EmitBytes&&) noexcept = default;
+
+ bool empty() const noexcept { return bytes_.empty(); }
+
+ void Append(absl::string_view more) noexcept {
+ absl::StrAppend(&bytes_, more);
+ }
+
+ void Emit(std::ostream&) const override;
+
+ bool operator==(const EmitBytes& other) const noexcept {
+ return bytes_ == other.bytes_;
+ }
+
+ friend std::ostream& operator<<(std::ostream& out,
+ const EmitBytes& emit) noexcept {
+ return out << "EmitBytes(" << emit.bytes_ << ')';
+ }
+
+ private:
+ std::string bytes_;
+};
+
+class EmitDelay final : public EmitTerm {
+ public:
+ explicit EmitDelay() noexcept = default;
+
+ explicit EmitDelay(absl::Duration delay) : delay_(std::move(delay)) {}
+
+ EmitDelay(const EmitDelay&) = default;
+ EmitDelay& operator=(const EmitDelay&) = default;
+ EmitDelay(EmitDelay&&) noexcept = default;
+ EmitDelay& operator=(EmitDelay&&) noexcept = default;
+
+ bool zero() const noexcept { return delay_ == absl::ZeroDuration(); }
+
+ void Increase(absl::Duration more) noexcept { delay_ += more; }
+
+ void Emit(std::ostream&) const noexcept override;
+
+ bool operator==(const EmitDelay& other) const noexcept {
+ return delay_ == other.delay_;
+ }
+
+ private:
+ absl::Duration delay_;
+};
+
+// Runs an entire program in the emitter EDSL.
+void Emit(absl::Span<const std::shared_ptr<const EmitTerm>>, std::ostream&);
+
+} // namespace goldfishterm_internal
+
+#endif // EC_GOLDFISHTERM_INTERNAL_EMIT_H_
diff --git a/goldfishterm/internal/string_capability.h b/goldfishterm/internal/string_capability.h
new file mode 100644
index 0000000..ddae964
--- /dev/null
+++ b/goldfishterm/internal/string_capability.h
@@ -0,0 +1,106 @@
+// 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.
+
+// Parses and interprets terminfo string capabilities. These are written in a
+// simple stack-based programming language whose semantics are documented in
+// terminfo(5).
+
+#ifndef EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_
+#define EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "goldfishterm/internal/emit.h"
+#include "goldfishterm/terminfo.h"
+#include "third_party/abseil/absl/types/optional.h"
+#include "third_party/abseil/absl/types/variant.h"
+
+namespace goldfishterm_internal {
+
+using StringCapabilityParameter = absl::variant<int, std::string>;
+
+struct InterpretStringCapabilityInput {
+ std::string capability;
+ std::vector<StringCapabilityParameter> parameters;
+
+ // The number of lines affected by this operation.
+ int lines_affected;
+
+ // The minimum baud rate at which padding should be expanded.
+ int padding_baud_rate;
+
+ // The padding character, or absl::nullopt if the terminal doesn't support
+ // padding.
+ absl::optional<char> pad_char;
+
+ // True iff the terminal supports XON/XOFF handshaking.
+ bool has_xon_xoff;
+
+ // The baud rate of the terminal.
+ int baud;
+
+ // The number of extra bits sent per character. For a terminal emulator, this
+ // is generally 0; for an RS-232 line, it's generally 2 or 3 (one start bit,
+ // one stop bit, and possibly one parity bit).
+ int extra_bits_per_character;
+
+ // Constructs an InterpretStringCapabilityInput with fail-safe defaults.
+ explicit InterpretStringCapabilityInput() noexcept
+ : lines_affected(1),
+ padding_baud_rate(0),
+ pad_char('\0'),
+ has_xon_xoff(false),
+ baud(9600),
+ extra_bits_per_character(0) {}
+
+ // Constructs an InterpretStringCapabilityInput from a terminfo entry.
+ explicit InterpretStringCapabilityInput(
+ const goldfishterm::TerminfoEntry& terminfo, std::string capability_,
+ std::vector<StringCapabilityParameter> parameters_, int baud_,
+ int lines_affected_) noexcept
+ : capability(std::move(capability_)),
+ parameters(std::move(parameters_)),
+ lines_affected(lines_affected_),
+ padding_baud_rate(
+ terminfo.get(goldfishterm::NumericCapability::kPaddingBaudRate)
+ .value_or(0)),
+ pad_char(terminfo.get(goldfishterm::StringCapability::kCharPadding)
+ .value_or(std::string(1, '\0'))[0]),
+ has_xon_xoff(terminfo.get(goldfishterm::BooleanCapability::kXonXoff)),
+ baud(baud_),
+ extra_bits_per_character(0) {}
+};
+
+struct InterpretStringCapabilityResult {
+ // Terms in the emitter EDSL (see emit.h).
+ std::vector<std::shared_ptr<const EmitTerm>> terms;
+
+ // The expected cost of emitting these terms. This is a unitless, arbitrarily
+ // scaled real number; it will be at least 0, but you should not rely on any
+ // absolute semantics.
+ float cost;
+};
+
+// Interprets a terminfo string capability, expanding parameters and inserting
+// padding and delays as appropriate. Throws std::runtime_error if parsing or
+// interpretation fails.
+InterpretStringCapabilityResult InterpretStringCapability(
+ const InterpretStringCapabilityInput&);
+
+} // namespace goldfishterm_internal
+
+#endif // EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_
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
diff --git a/goldfishterm/internal/string_capability_test.cc b/goldfishterm/internal/string_capability_test.cc
new file mode 100644
index 0000000..a821551
--- /dev/null
+++ b/goldfishterm/internal/string_capability_test.cc
@@ -0,0 +1,372 @@
+// 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 "goldfishterm/internal/string_capability.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <string.h>
+
+#include <ostream>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "goldfishterm/internal/emit.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/time.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace goldfishterm_internal {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Pointee;
+using ::testing::Property;
+using ::testing::StrEq;
+using ::testing::WhenDynamicCastTo;
+
+MATCHER_P(PointsToEmitBytes, s,
+ absl::StrCat("points to EmitBytes('", s, "')")) {
+ const char* start;
+ int length;
+ if constexpr (std::is_same_v<decltype(s), const char* const>) {
+ start = s;
+ length = strlen(s);
+ } else {
+ start = s.data();
+ length = s.size();
+ }
+ return ExplainMatchResult(Pointee(WhenDynamicCastTo<const EmitBytes&>(
+ EmitBytes(absl::string_view(start, length)))),
+ arg, result_listener);
+}
+
+MATCHER_P(PointsToEmitDelay, t,
+ absl::StrCat("points to EmitDelay('", absl::FormatDuration(t),
+ "')")) {
+ return ExplainMatchResult(
+ Pointee(WhenDynamicCastTo<const EmitDelay&>(EmitDelay(t))), arg,
+ result_listener);
+}
+
+InterpretStringCapabilityResult Interpret(
+ std::string text, std::vector<StringCapabilityParameter> parameters = {}) {
+ InterpretStringCapabilityInput input;
+ input.capability = std::move(text);
+ input.parameters = std::move(parameters);
+ return InterpretStringCapability(input);
+}
+
+TEST(InterpretStringCapabilityTest, PadInteger) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.baud = 8'000; // 1 character every millisecond
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, PadFractional) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<.9>";
+ input.baud = 80'000; // 10 characters every millisecond
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(9, '\0'))));
+ EXPECT_EQ(result.cost, 9);
+}
+
+TEST(InterpretStringCapabilityTest, PadIgnoredIfMultipleSignificantFigures) {
+ EXPECT_THAT(Interpret("$<1.1>").terms,
+ ElementsAre(PointsToEmitBytes("$<1.1>")));
+}
+
+TEST(InterpretStringCapabilityTest, PerLinePad) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<4*>";
+ input.lines_affected = 10;
+ input.baud = 8'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, PadIgnoredIfTerminalTooSlow) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.padding_baud_rate = 115200;
+ input.baud = 9600;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms, IsEmpty());
+ EXPECT_EQ(result.cost, 0);
+}
+
+TEST(InterpretStringCapabilityTest, PadWithWeirdCharacter) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<.1>";
+ input.pad_char = 'a';
+ input.baud = 9600;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("a")));
+ EXPECT_EQ(result.cost, 1);
+}
+
+TEST(InterpretStringCapabilityTest, SleepIfPadUnsupported) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.pad_char = absl::nullopt;
+ input.baud = 16'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitDelay(absl::Milliseconds(40))));
+ EXPECT_EQ(result.cost, 80); // the equivalent of 80 characters
+}
+
+TEST(InterpretStringCapabilityTest, PadWith8N1) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.baud = 10'000; // 1 character every millisecond using
+ input.extra_bits_per_character = 2; // one start and one stop bit
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, PadIgnoredIfXonXoffAvailable) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.has_xon_xoff = true;
+ input.baud = 8'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms, IsEmpty());
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, MandatoryPadSentEvenIfXonXoffAvailable) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40/>";
+ input.has_xon_xoff = true;
+ input.baud = 8'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, Empty) {
+ InterpretStringCapabilityResult result = Interpret("");
+ EXPECT_THAT(result.terms, IsEmpty());
+ EXPECT_EQ(result.cost, 0);
+}
+
+TEST(InterpretStringCapabilityTest, EmitLiteralPercent) {
+ InterpretStringCapabilityResult result = Interpret("%%");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("%")));
+ EXPECT_EQ(result.cost, 1);
+}
+
+TEST(InterpretStringCapabilityTest, EmitChar) {
+ InterpretStringCapabilityResult result = Interpret("a");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("a")));
+ EXPECT_EQ(result.cost, 1);
+}
+
+TEST(InterpretStringCapabilityTest, EmitText) {
+ InterpretStringCapabilityResult result = Interpret("hi");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("hi")));
+ EXPECT_EQ(result.cost, 2);
+}
+
+TEST(InterpretStringCapabilityTest, EmitNonprinting) {
+ InterpretStringCapabilityResult result = Interpret("\r\n");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("\r\n")));
+ EXPECT_EQ(result.cost, 2);
+}
+
+TEST(InterpretStringCapabilityTest, PushParameter) {
+ EXPECT_THAT(Interpret("%p2%d", {42, 96}).terms,
+ ElementsAre(PointsToEmitBytes("96")));
+}
+
+TEST(InterpretStringCapabilityTest, PopNumber) {
+ EXPECT_THAT(Interpret("%p1%:-10.5o", {42}).terms,
+ ElementsAre(PointsToEmitBytes("00052 ")));
+}
+
+TEST(InterpretStringCapabilityTest, PopString) {
+ EXPECT_THAT(Interpret("%p1%:-10s", {"foo"}).terms,
+ ElementsAre(PointsToEmitBytes("foo ")));
+}
+
+TEST(InterpretStringCapabilityTest, PopChar) {
+ EXPECT_THAT(Interpret("%p1%:-10c", {'x'}).terms,
+ ElementsAre(PointsToEmitBytes("x ")));
+}
+
+TEST(InterpretStringCapabilityTest, StoreRecall) {
+ EXPECT_THAT(Interpret("%p1%Pa%p2%Pb%gb%s,%ga%d", {1, "foo"}).terms,
+ ElementsAre(PointsToEmitBytes("foo,1")));
+}
+
+TEST(InterpretStringCapabilityTest, PushChar) {
+ EXPECT_THAT(Interpret("%'x'%'y'%c%c").terms,
+ ElementsAre(PointsToEmitBytes("yx")));
+}
+
+TEST(InterpretStringCapabilityTest, PushNumber) {
+ EXPECT_THAT(Interpret("%{123}%d").terms,
+ ElementsAre(PointsToEmitBytes("123")));
+}
+
+TEST(InterpretStringCapabilityTest, Strlen) {
+ EXPECT_THAT(Interpret("%p1%l%d", {"foobar"}).terms,
+ ElementsAre(PointsToEmitBytes("6")));
+}
+
+TEST(InterpretStringCapabilityTest, Add) {
+ EXPECT_THAT(Interpret("%{5}%{2}%+%d").terms,
+ ElementsAre(PointsToEmitBytes("7")));
+}
+
+TEST(InterpretStringCapabilityTest, Subtract) {
+ EXPECT_THAT(Interpret("%{5}%{2}%-%d").terms,
+ ElementsAre(PointsToEmitBytes("3")));
+}
+
+TEST(InterpretStringCapabilityTest, Multiply) {
+ EXPECT_THAT(Interpret("%{5}%{2}%*%d").terms,
+ ElementsAre(PointsToEmitBytes("10")));
+}
+
+TEST(InterpretStringCapabilityTest, Divide) {
+ EXPECT_THAT(Interpret("%{5}%{2}%/%d").terms,
+ ElementsAre(PointsToEmitBytes("2")));
+}
+
+TEST(InterpretStringCapabilityTest, Modulo) {
+ EXPECT_THAT(Interpret("%{5}%{2}%m%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseAnd) {
+ EXPECT_THAT(Interpret("%{3}%{15}%&%d").terms,
+ ElementsAre(PointsToEmitBytes("3")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseOr) {
+ EXPECT_THAT(Interpret("%{2}%{1}%|%d").terms,
+ ElementsAre(PointsToEmitBytes("3")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseXor) {
+ EXPECT_THAT(Interpret("%{15}%{10}%^%d").terms,
+ ElementsAre(PointsToEmitBytes("5")));
+}
+
+TEST(InterpretStringCapabilityTest, Equals) {
+ EXPECT_THAT(Interpret("%{15}%{15}%=%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{15}%{14}%=%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, LessThan) {
+ EXPECT_THAT(Interpret("%{2}%{3}%<%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{2}%{2}%<%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, GreaterThan) {
+ EXPECT_THAT(Interpret("%{3}%{2}%>%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{2}%{2}%>%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, LogicalAnd) {
+ EXPECT_THAT(Interpret("%{0}%{0}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{0}%{1}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{1}%{0}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{1}%{1}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+}
+
+TEST(InterpretStringCapabilityTest, LogicalOr) {
+ EXPECT_THAT(Interpret("%{0}%{0}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{0}%{1}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{1}%{0}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{1}%{1}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+}
+
+TEST(InterpretStringCapabilityTest, LogicalNot) {
+ EXPECT_THAT(Interpret("%{0}%!%d").terms, ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{5}%!%d").terms, ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseNot) {
+ EXPECT_THAT(Interpret("%{8}%~%d").terms,
+ ElementsAre(PointsToEmitBytes("-9")));
+}
+
+TEST(InterpretStringCapabilityTest, IncrementParameters) {
+ EXPECT_THAT(Interpret("%i%p1%d,%p2%d", {6, 8}).terms,
+ ElementsAre(PointsToEmitBytes("7,9")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThen) {
+ EXPECT_THAT(Interpret("%?%{0}%ttrue%;").terms, IsEmpty());
+ EXPECT_THAT(Interpret("%?%{1}%ttrue%;").terms,
+ ElementsAre(PointsToEmitBytes("true")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElse) {
+ EXPECT_THAT(Interpret("%?%{0}%ttrue%efalse%;").terms,
+ ElementsAre(PointsToEmitBytes("false")));
+ EXPECT_THAT(Interpret("%?%{1}%ttrue%efalse%;").terms,
+ ElementsAre(PointsToEmitBytes("true")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElseThen) {
+ EXPECT_THAT(Interpret("%?%{0}%t!%e%{1}%tgood%;").terms,
+ ElementsAre(PointsToEmitBytes("good")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElseThenElse) {
+ EXPECT_THAT(Interpret("%?%{0}%t!%e%{0}%t!%egood%;").terms,
+ ElementsAre(PointsToEmitBytes("good")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElseThenElseShortCircuit) {
+ EXPECT_THAT(Interpret("%?%{1}%tgood%e%{0}%t!%e!%;").terms,
+ ElementsAre(PointsToEmitBytes("good")));
+}
+
+} // namespace
+} // namespace goldfishterm_internal