diff --git a/arc/cpp/stl/str/str.h b/arc/cpp/stl/str/str.h index d7843bd4b7..54c9f9143e 100644 --- a/arc/cpp/stl/str/str.h +++ b/arc/cpp/stl/str/str.h @@ -11,7 +11,9 @@ #include #include +#include #include +#include #include #include @@ -38,11 +40,498 @@ std::string format_float(T v) { return {buf, end}; } +/// Parsed Go-style format spec ([flags][width][.precision][verb]). +struct FormatSpec { + bool alt = false; + bool plus = false; + bool minus = false; + bool space = false; + bool zero = false; + int width = -1; + int precision = -1; + char verb = '\0'; +}; + +inline FormatSpec parse_format_spec(const std::string &s) { + FormatSpec f; + size_t i = 0; + bool flags_done = false; + while (i < s.size() && !flags_done) { + switch (s[i]) { + case '#': + f.alt = true; + ++i; + break; + case '+': + f.plus = true; + ++i; + break; + case '-': + f.minus = true; + ++i; + break; + case ' ': + f.space = true; + ++i; + break; + case '0': + f.zero = true; + ++i; + break; + default: + flags_done = true; + break; + } + } + if (i < s.size() && s[i] >= '0' && s[i] <= '9') { + f.width = 0; + while (i < s.size() && s[i] >= '0' && s[i] <= '9') { + f.width = f.width * 10 + (s[i] - '0'); + ++i; + } + } + if (i < s.size() && s[i] == '.') { + ++i; + f.precision = 0; + while (i < s.size() && s[i] >= '0' && s[i] <= '9') { + f.precision = f.precision * 10 + (s[i] - '0'); + ++i; + } + } + if (i < s.size()) f.verb = s[i]; + return f; +} + +/// Encodes a Unicode code point as UTF-8. Mirrors Go's behavior: invalid +/// code points (negative, > U+10FFFF, or surrogates) become U+FFFD. +inline std::string utf8_encode(int64_t cp) { + std::string out; + if (cp < 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + out += "\xEF\xBF\xBD"; + return out; + } + if (cp < 0x80) { + out += static_cast(cp); + } else if (cp < 0x800) { + out += static_cast(0xC0 | (cp >> 6)); + out += static_cast(0x80 | (cp & 0x3F)); + } else if (cp < 0x10000) { + out += static_cast(0xE0 | (cp >> 12)); + out += static_cast(0x80 | ((cp >> 6) & 0x3F)); + out += static_cast(0x80 | (cp & 0x3F)); + } else { + out += static_cast(0xF0 | (cp >> 18)); + out += static_cast(0x80 | ((cp >> 12) & 0x3F)); + out += static_cast(0x80 | ((cp >> 6) & 0x3F)); + out += static_cast(0x80 | (cp & 0x3F)); + } + return out; +} + +/// Counts UTF-8 code points in s. Each byte whose top two bits are not 10 +/// starts a new rune, so this also counts invalid bytes as one rune each +/// (matching Go's tolerance for non-UTF-8 input). +inline int utf8_rune_count(const std::string &s) { + int count = 0; + for (const unsigned char c: s) + if ((c & 0xC0) != 0x80) ++count; + return count; +} + +/// Returns the byte prefix of s covering the first n runes. Used for Go-style +/// precision truncation on %s and %q, which counts code points, not bytes. +inline std::string utf8_truncate(const std::string &s, int n) { + if (n <= 0) return ""; + int count = 0; + for (size_t i = 0; i < s.size(); ++i) { + const unsigned char c = static_cast(s[i]); + if ((c & 0xC0) != 0x80) { + if (count == n) return s.substr(0, i); + ++count; + } + } + return s; +} + +/// Converts an unsigned magnitude to a string in the given base (2, 8, 10, 16). +inline std::string convert_base(uint64_t v, int base, bool upper) { + if (v == 0) return "0"; + const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + std::string out; + while (v) { + out.insert(out.begin(), digits[v % static_cast(base)]); + v /= static_cast(base); + } + return out; +} + +/// Pads and assembles a formatted value (sign + prefix + body) to the +/// requested width. Honors the `-` and `0` flags per Go semantics. +inline std::string apply_width( + const std::string &sign, + const std::string &prefix, + const std::string &body, + const FormatSpec &f +) { + const int sz = static_cast(sign.size() + prefix.size() + body.size()); + if (f.width <= 0 || sz >= f.width) return sign + prefix + body; + const int pad = f.width - sz; + if (f.minus) return sign + prefix + body + std::string(pad, ' '); + // Zero flag is suppressed when precision is explicitly set (Go behavior). + if (f.zero && f.precision < 0) return sign + prefix + std::string(pad, '0') + body; + return std::string(pad, ' ') + sign + prefix + body; +} + +/// Formats an integer value to match Go's fmt.Sprintf("%"+spec, v). When +/// is_signed is false, value is reinterpreted as uint64_t. +inline std::string +format_int_value(const std::string &spec_str, int64_t value, bool is_signed) { + if (spec_str.empty()) + return is_signed ? std::to_string(value) + : std::to_string(static_cast(value)); + const FormatSpec f = parse_format_spec(spec_str); + const bool neg = is_signed && value < 0; + uint64_t abs_v; + if (is_signed) { + if (value == std::numeric_limits::min()) + abs_v = static_cast(std::numeric_limits::max()) + 1; + else + abs_v = static_cast(neg ? -value : value); + } else { + abs_v = static_cast(value); + } + std::string sign; + if (neg) + sign = "-"; + else if (f.plus) + sign = "+"; + else if (f.space) + sign = " "; + + std::string prefix; + std::string body; + int base = 10; + bool upper = false; + std::string alt_prefix; + bool alt_applies = false; + switch (f.verb) { + case 'd': + break; + case 'b': + base = 2; + alt_prefix = "0b"; + alt_applies = true; + break; + case 'o': + base = 8; + alt_prefix = "0"; + alt_applies = true; + break; + case 'O': + base = 8; + prefix = "0o"; + break; + case 'x': + base = 16; + alt_prefix = "0x"; + alt_applies = true; + break; + case 'X': + base = 16; + upper = true; + alt_prefix = "0X"; + alt_applies = true; + break; + case 'c': { + // %c outputs one rune; width is in rune count, not bytes. + std::string body_c = utf8_encode(value); + if (f.width <= 1) return body_c; + const int pad = f.width - 1; + if (f.minus) return body_c + std::string(pad, ' '); + return std::string(pad, ' ') + body_c; + } + case 'q': { + // %q on a rune mirrors Go's strconv.QuoteRune: wrap in single + // quotes and escape control chars / DEL; printable code points + // pass through as UTF-8. + std::string body_q = "'"; + bool special = true; + switch (value) { + case '\\': + body_q += "\\\\"; + break; + case '\'': + body_q += "\\'"; + break; + case '\a': + body_q += "\\a"; + break; + case '\b': + body_q += "\\b"; + break; + case '\f': + body_q += "\\f"; + break; + case '\n': + body_q += "\\n"; + break; + case '\r': + body_q += "\\r"; + break; + case '\t': + body_q += "\\t"; + break; + case '\v': + body_q += "\\v"; + break; + default: + special = false; + break; + } + if (!special) { + if (value >= 0x20 && value < 0x7F) { + body_q += static_cast(value); + } else if (value < 0x80) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\x%02x", static_cast(value)); + body_q += buf; + } else { + body_q += utf8_encode(value); + } + } + body_q += "'"; + if (f.width <= 0) return body_q; + const int body_runes = utf8_rune_count(body_q); + if (body_runes >= f.width) return body_q; + const int pad = f.width - body_runes; + if (f.minus) return body_q + std::string(pad, ' '); + return std::string(pad, ' ') + body_q; + } + case 'U': { + std::string hex = convert_base(abs_v, 16, true); + if (hex.size() < 4) hex = std::string(4 - hex.size(), '0') + hex; + return apply_width("", "U+", hex, f); + } + default: { + std::string raw = is_signed ? std::to_string(value) + : std::to_string(static_cast(value)); + return "%!" + std::string(1, f.verb) + "(int=" + raw + ")"; + } + } + body = convert_base(abs_v, base, upper); + // Go suppresses the '#' prefix only on `%#o` of zero, because the body + // "0" already carries the implicit octal prefix. `%#x`, `%#X`, and + // `%#b` keep their prefixes (0x0, 0X0, 0b0) even when the value is 0. + if (f.alt && alt_applies && !(f.verb == 'o' && abs_v == 0)) prefix = alt_prefix; + if (f.precision > 0 && static_cast(body.size()) < f.precision) + body = std::string(f.precision - body.size(), '0') + body; + else if (f.precision == 0 && abs_v == 0) + body = ""; + return apply_width(sign, prefix, body, f); +} + +/// Formats a float value to match Go's fmt.Sprintf("%"+spec, v). Delegates +/// finite values to snprintf; NaN/Β±Inf use Go's capitalization. +inline std::string format_float_value(const std::string &spec_str, double v) { + if (spec_str.empty()) return format_float(v); + const FormatSpec f = parse_format_spec(spec_str); + if (std::isnan(v)) { + FormatSpec g = f; + g.zero = false; + std::string sign; + if (f.plus) + sign = "+"; + else if (f.space) + sign = " "; + return apply_width(sign, "", "NaN", g); + } + if (std::isinf(v)) { + const bool neg = v < 0; + std::string sign; + if (neg) + sign = "-"; + else if (f.space) + sign = " "; + else + sign = "+"; + FormatSpec g = f; + g.zero = false; + return apply_width(sign, "", "Inf", g); + } + // Go's %g/%G with default precision uses shortest roundtrip. C's snprintf + // defaults to 6 significant digits, so route those cases through + // format_float() which uses std::to_chars(general). + if ((f.verb == 'g' || f.verb == 'G') && f.precision < 0) { + std::string body = format_float(v); + if (f.verb == 'G') + for (auto &c: body) + if (c == 'e') { + c = 'E'; + break; + } + std::string sign; + if (!body.empty() && (body[0] == '-' || body[0] == '+')) { + sign = body.substr(0, 1); + body = body.substr(1); + } else if (f.plus) { + sign = "+"; + } else if (f.space) { + sign = " "; + } + return apply_width(sign, "", body, f); + } + char buf[128]; + const int prec = f.precision >= 0 ? f.precision : 6; + int n = 0; + switch (f.verb) { + case 'f': + case 'F': + n = f.alt ? std::snprintf(buf, sizeof(buf), "%#.*f", prec, v) + : std::snprintf(buf, sizeof(buf), "%.*f", prec, v); + break; + case 'e': + n = f.alt ? std::snprintf(buf, sizeof(buf), "%#.*e", prec, v) + : std::snprintf(buf, sizeof(buf), "%.*e", prec, v); + break; + case 'E': + n = f.alt ? std::snprintf(buf, sizeof(buf), "%#.*E", prec, v) + : std::snprintf(buf, sizeof(buf), "%.*E", prec, v); + break; + case 'g': + n = f.alt ? std::snprintf(buf, sizeof(buf), "%#.*g", prec, v) + : std::snprintf(buf, sizeof(buf), "%.*g", prec, v); + break; + case 'G': + n = f.alt ? std::snprintf(buf, sizeof(buf), "%#.*G", prec, v) + : std::snprintf(buf, sizeof(buf), "%.*G", prec, v); + break; + default: + return ""; + } + if (n < 0 || n >= static_cast(sizeof(buf))) return ""; + std::string body(buf, n); + std::string sign; + if (!body.empty() && (body[0] == '-' || body[0] == '+')) { + sign = body.substr(0, 1); + body = body.substr(1); + } else if (f.plus) { + sign = "+"; + } else if (f.space) { + sign = " "; + } + FormatSpec g = f; + g.precision = -1; + return apply_width(sign, "", body, g); +} + +/// Quotes a string with Go's strconv.Quote semantics. Bytes >= 0x80 pass +/// through, so callers are responsible for UTF-8 validity. +inline std::string go_quote(const std::string &s) { + std::string out; + out.reserve(s.size() + 2); + out += '"'; + for (const unsigned char c: s) { + switch (c) { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\a': + out += "\\a"; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + case '\v': + out += "\\v"; + break; + default: + if (c >= 0x20 && c < 0x7F) { + out += static_cast(c); + } else if (c >= 0x80) { + out += static_cast(c); + } else { + char hexbuf[8]; + std::snprintf(hexbuf, sizeof(hexbuf), "\\x%02x", c); + out += hexbuf; + } + break; + } + } + out += '"'; + return out; +} + +inline std::string +format_string_value(const std::string &spec_str, const std::string &v) { + if (spec_str.empty()) return v; + const FormatSpec f = parse_format_spec(spec_str); + // Precision for %s and %q truncates the input by runes (Go semantics). + // Precision is ignored for %x/%X, matching Go. + std::string truncated = v; + if (f.precision >= 0 && (f.verb == 's' || f.verb == 'q')) + truncated = utf8_truncate(v, f.precision); + std::string body; + switch (f.verb) { + case 's': + body = truncated; + break; + case 'q': + body = go_quote(truncated); + break; + case 'x': + case 'X': { + const bool upper = f.verb == 'X'; + const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + body.reserve(truncated.size() * 2); + for (const unsigned char c: truncated) { + body += digits[c >> 4]; + body += digits[c & 0xF]; + } + break; + } + default: + return "%!" + std::string(1, f.verb) + "(string=" + v + ")"; + } + // Width compares against rune count for %s/%q, byte count for %x/%X. + if (f.width <= 0) return body; + const int body_len = (f.verb == 's' || f.verb == 'q') + ? utf8_rune_count(body) + : static_cast(body.size()); + if (body_len >= f.width) return body; + const int pad = f.width - body_len; + if (f.minus) return body + std::string(pad, ' '); + return std::string(pad, ' ') + body; +} + class Module : public stl::Module { std::shared_ptr str_state; wasmtime::Store *store = nullptr; wasmtime::Memory *memory = nullptr; + /// Reads `len` bytes at `ptr` from WASM memory. Returns empty string if + /// memory has not been bound or the read would be out of bounds. + std::string read_memory_string(uint32_t ptr, uint32_t len) const { + if (!this->memory || !this->store) return ""; + const auto mem_span = this->memory->data(*this->store); + if (static_cast(ptr) + len > mem_span.size()) return ""; + return {reinterpret_cast(mem_span.data() + ptr), len}; + } + public: explicit Module(std::shared_ptr str_state): str_state(std::move(str_state)) {} @@ -156,6 +645,82 @@ class Module : public stl::Module { [ss](double v) -> uint32_t { return ss->create(format_float(v)); } ) .unwrap(); + linker + .func_wrap( + "string", + "format_i32", + [self, ss](int32_t v, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create( + format_int_value(spec, static_cast(v), true) + ); + } + ) + .unwrap(); + linker + .func_wrap( + "string", + "format_u32", + [self, ss](uint32_t v, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create( + format_int_value(spec, static_cast(v), false) + ); + } + ) + .unwrap(); + linker + .func_wrap( + "string", + "format_i64", + [self, ss](int64_t v, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create(format_int_value(spec, v, true)); + } + ) + .unwrap(); + linker + .func_wrap( + "string", + "format_u64", + [self, ss](uint64_t v, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create( + format_int_value(spec, static_cast(v), false) + ); + } + ) + .unwrap(); + linker + .func_wrap( + "string", + "format_f32", + [self, ss](float v, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create(format_float_value(spec, static_cast(v))); + } + ) + .unwrap(); + linker + .func_wrap( + "string", + "format_f64", + [self, ss](double v, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create(format_float_value(spec, v)); + } + ) + .unwrap(); + linker + .func_wrap( + "string", + "format_string", + [self, ss](uint32_t handle, uint32_t ptr, uint32_t len) -> uint32_t { + const std::string spec = self->read_memory_string(ptr, len); + return ss->create(format_string_value(spec, ss->get(handle))); + } + ) + .unwrap(); } void set_wasm_context(wasmtime::Store *store, wasmtime::Memory *memory) override { diff --git a/arc/cpp/stl/str/str_test.cpp b/arc/cpp/stl/str/str_test.cpp index 759e6a25a6..caae5ba140 100644 --- a/arc/cpp/stl/str/str_test.cpp +++ b/arc/cpp/stl/str/str_test.cpp @@ -7,6 +7,8 @@ // License, use of this software will be governed by the Apache License, Version 2.0, // included in the file licenses/APL.txt. +#include +#include #include #include #include @@ -19,6 +21,12 @@ #include "wasmtime.hh" namespace arc::stl::str { + +/// Offset in WASM linear memory where tests stage format-spec bytes before +/// invoking a format_* host function. Chosen to avoid the literal data +/// placed at offsets 0 and 5. +constexpr uint32_t SPEC_OFFSET = 100; + const std::string_view STR_WAT = R"wat( (module (import "string" "from_literal" (func $from_lit (param i32 i32) (result i32))) @@ -31,6 +39,13 @@ const std::string_view STR_WAT = R"wat( (import "string" "from_u64" (func $from_u64 (param i64) (result i32))) (import "string" "from_f32" (func $from_f32 (param f32) (result i32))) (import "string" "from_f64" (func $from_f64 (param f64) (result i32))) + (import "string" "format_i32" (func $format_i32 (param i32 i32 i32) (result i32))) + (import "string" "format_u32" (func $format_u32 (param i32 i32 i32) (result i32))) + (import "string" "format_i64" (func $format_i64 (param i64 i32 i32) (result i32))) + (import "string" "format_u64" (func $format_u64 (param i64 i32 i32) (result i32))) + (import "string" "format_f32" (func $format_f32 (param f32 i32 i32) (result i32))) + (import "string" "format_f64" (func $format_f64 (param f64 i32 i32) (result i32))) + (import "string" "format_string" (func $format_string (param i32 i32 i32) (result i32))) (memory (export "memory") 1) (data (i32.const 0) "hello") (data (i32.const 5) " world") @@ -58,6 +73,20 @@ const std::string_view STR_WAT = R"wat( (call $from_f32 (local.get 0))) (func (export "call_from_f64") (param f64) (result i32) (call $from_f64 (local.get 0))) + (func (export "call_format_i32") (param i32 i32 i32) (result i32) + (call $format_i32 (local.get 0) (local.get 1) (local.get 2))) + (func (export "call_format_u32") (param i32 i32 i32) (result i32) + (call $format_u32 (local.get 0) (local.get 1) (local.get 2))) + (func (export "call_format_i64") (param i64 i32 i32) (result i32) + (call $format_i64 (local.get 0) (local.get 1) (local.get 2))) + (func (export "call_format_u64") (param i64 i32 i32) (result i32) + (call $format_u64 (local.get 0) (local.get 1) (local.get 2))) + (func (export "call_format_f32") (param f32 i32 i32) (result i32) + (call $format_f32 (local.get 0) (local.get 1) (local.get 2))) + (func (export "call_format_f64") (param f64 i32 i32) (result i32) + (call $format_f64 (local.get 0) (local.get 1) (local.get 2))) + (func (export "call_format_string") (param i32 i32 i32) (result i32) + (call $format_string (local.get 0) (local.get 1) (local.get 2))) ) )wat"; @@ -84,6 +113,12 @@ struct StrModuleFixture { return std::get(*instance.get(store, name)); } + void write_spec(const std::string &spec) { + const auto mem_span = memory.data(store); + auto *mem = const_cast(mem_span.data()); + std::memcpy(mem + SPEC_OFFSET, spec.data(), spec.size()); + } + private: wasmtime::Instance setup_instance() { mod.bind_to(linker, store); @@ -247,6 +282,7 @@ TEST(StrModule, FromF32FormatsShortestRoundTrip) { EXPECT_EQ(call_from(f, "call_from_f32", 100.0f), "100"); EXPECT_EQ(call_from(f, "call_from_f32", -2.5f), "-2.5"); EXPECT_EQ(call_from(f, "call_from_f32", 42.5f), "42.5"); + EXPECT_EQ(call_from(f, "call_from_f32", std::copysign(0.0f, -1.0f)), "-0"); } TEST(StrModule, FromF64FormatsShortestRoundTrip) { @@ -259,6 +295,7 @@ TEST(StrModule, FromF64FormatsShortestRoundTrip) { call_from(f, "call_from_f64", 0.1234567890123456), "0.1234567890123456" ); + EXPECT_EQ(call_from(f, "call_from_f64", std::copysign(0.0, -1.0)), "-0"); } TEST(StrModule, FromF64HandlesNaNAndInfinityWithGoCapitalization) { @@ -292,4 +329,261 @@ TEST(StrModule, FromF32HandlesNaNAndInfinityWithGoCapitalization) { "-Inf" ); } + +template +static std::string call_format( + StrModuleFixture &f, + const std::string &fn_name, + ValT v, + const std::string &spec +) { + f.write_spec(spec); + auto fn = f.get_func(fn_name); + auto result = fn.call( + f.store, + {wasmtime::Val(v), + wasmtime::Val(static_cast(SPEC_OFFSET)), + wasmtime::Val(static_cast(spec.size()))} + ) + .unwrap(); + return f.state->get(result[0].i32()); +} + +TEST(StrModule, FormatI32MatchesGoFmtSprintf) { + StrModuleFixture f; + EXPECT_EQ(call_format(f, "call_format_i32", 42, "d"), "42"); + EXPECT_EQ(call_format(f, "call_format_i32", -42, "d"), "-42"); + EXPECT_EQ(call_format(f, "call_format_i32", 42, "5d"), " 42"); + EXPECT_EQ(call_format(f, "call_format_i32", 42, "-5d"), "42 "); + EXPECT_EQ(call_format(f, "call_format_i32", 42, "05d"), "00042"); + EXPECT_EQ(call_format(f, "call_format_i32", -42, "05d"), "-0042"); + EXPECT_EQ(call_format(f, "call_format_i32", 42, "+d"), "+42"); + EXPECT_EQ(call_format(f, "call_format_i32", 42, " d"), " 42"); + EXPECT_EQ(call_format(f, "call_format_i32", 255, "x"), "ff"); + EXPECT_EQ(call_format(f, "call_format_i32", 255, "X"), "FF"); + EXPECT_EQ(call_format(f, "call_format_i32", 255, "#x"), "0xff"); + EXPECT_EQ(call_format(f, "call_format_i32", -255, "x"), "-ff"); + EXPECT_EQ(call_format(f, "call_format_i32", -255, "#x"), "-0xff"); + EXPECT_EQ(call_format(f, "call_format_i32", 5, "b"), "101"); + EXPECT_EQ(call_format(f, "call_format_i32", -5, "b"), "-101"); + EXPECT_EQ(call_format(f, "call_format_i32", 5, "#b"), "0b101"); + EXPECT_EQ(call_format(f, "call_format_i32", 8, "o"), "10"); + EXPECT_EQ(call_format(f, "call_format_i32", 8, "#o"), "010"); + EXPECT_EQ(call_format(f, "call_format_i32", 8, "O"), "0o10"); + EXPECT_EQ(call_format(f, "call_format_i32", 65, "c"), "A"); +} + +TEST(StrModule, FormatU32MatchesGoFmtSprintf) { + StrModuleFixture f; + EXPECT_EQ(call_format(f, "call_format_u32", 255, "x"), "ff"); + EXPECT_EQ(call_format(f, "call_format_u32", 255, "X"), "FF"); + EXPECT_EQ(call_format(f, "call_format_u32", 255, "#x"), "0xff"); + EXPECT_EQ(call_format(f, "call_format_u32", 255, "08x"), "000000ff"); + EXPECT_EQ(call_format(f, "call_format_u32", 255, ".4x"), "00ff"); + EXPECT_EQ(call_format(f, "call_format_u32", 0, "x"), "0"); + EXPECT_EQ(call_format(f, "call_format_u32", 0, "#x"), "0x0"); + EXPECT_EQ(call_format(f, "call_format_u32", 5, "b"), "101"); + EXPECT_EQ(call_format(f, "call_format_u32", 0, ".0d"), ""); + EXPECT_EQ( + call_format( + f, + "call_format_u32", + static_cast(0xFFFFFFFFU), + "x" + ), + "ffffffff" + ); +} + +TEST(StrModule, FormatI64MatchesGoFmtSprintf) { + StrModuleFixture f; + EXPECT_EQ( + call_format(f, "call_format_i64", 9223372036854775807LL, "d"), + "9223372036854775807" + ); + EXPECT_EQ( + call_format( + f, + "call_format_i64", + std::numeric_limits::min(), + "d" + ), + "-9223372036854775808" + ); + EXPECT_EQ(call_format(f, "call_format_i64", -1LL, "x"), "-1"); + EXPECT_EQ( + call_format(f, "call_format_i64", 0xDEADBEEFLL, "x"), + "deadbeef" + ); + EXPECT_EQ( + call_format(f, "call_format_i64", 0xDEADBEEFLL, "X"), + "DEADBEEF" + ); +} + +TEST(StrModule, FormatU64MatchesGoFmtSprintf) { + StrModuleFixture f; + EXPECT_EQ( + call_format( + f, + "call_format_u64", + static_cast(0xFFFFFFFFFFFFFFFFULL), + "x" + ), + "ffffffffffffffff" + ); + EXPECT_EQ( + call_format( + f, + "call_format_u64", + static_cast(0xFFFFFFFFFFFFFFFFULL), + "d" + ), + "18446744073709551615" + ); +} + +TEST(StrModule, FormatF32MatchesGoFmtSprintf) { + StrModuleFixture f; + EXPECT_EQ(call_format(f, "call_format_f32", 3.14f, ".2f"), "3.14"); + EXPECT_EQ(call_format(f, "call_format_f32", 1.5f, ".1f"), "1.5"); + EXPECT_EQ(call_format(f, "call_format_f32", 1.5f, "10.1f"), " 1.5"); + EXPECT_EQ(call_format(f, "call_format_f32", 1.5f, "-10.1f"), "1.5 "); + EXPECT_EQ(call_format(f, "call_format_f32", 1.5f, "010.1f"), "00000001.5"); + EXPECT_EQ(call_format(f, "call_format_f32", 1.5f, "+.1f"), "+1.5"); + EXPECT_EQ(call_format(f, "call_format_f32", -1.5f, ".1f"), "-1.5"); +} + +TEST(StrModule, FormatF64MatchesGoFmtSprintf) { + StrModuleFixture f; + EXPECT_EQ(call_format(f, "call_format_f64", 3.14159, ".2f"), "3.14"); + EXPECT_EQ(call_format(f, "call_format_f64", 1.5, ".1f"), "1.5"); + EXPECT_EQ(call_format(f, "call_format_f64", 2.71828, ".3f"), "2.718"); + EXPECT_EQ(call_format(f, "call_format_f64", 1.0e6, ".2e"), "1.00e+06"); + EXPECT_EQ(call_format(f, "call_format_f64", 1.0e6, ".2E"), "1.00E+06"); + EXPECT_EQ( + call_format(f, "call_format_f64", 1234567.89, "g"), + "1.23456789e+06" + ); +} + +TEST(StrModule, FormatF64HandlesNaNAndInfinityWithGoCapitalization) { + StrModuleFixture f; + const double nan = std::numeric_limits::quiet_NaN(); + const double pos_inf = std::numeric_limits::infinity(); + const double neg_inf = -pos_inf; + EXPECT_EQ(call_format(f, "call_format_f64", nan, "f"), "NaN"); + EXPECT_EQ(call_format(f, "call_format_f64", pos_inf, "f"), "+Inf"); + EXPECT_EQ(call_format(f, "call_format_f64", neg_inf, "f"), "-Inf"); + EXPECT_EQ(call_format(f, "call_format_f64", nan, "5f"), " NaN"); + EXPECT_EQ(call_format(f, "call_format_f64", pos_inf, "6f"), " +Inf"); + EXPECT_EQ(call_format(f, "call_format_f64", pos_inf, "05f"), " +Inf"); +} + +static std::string +call_format_string(StrModuleFixture &f, int32_t handle, const std::string &spec) { + f.write_spec(spec); + auto fn = f.get_func("call_format_string"); + auto result = fn.call( + f.store, + {wasmtime::Val(handle), + wasmtime::Val(static_cast(SPEC_OFFSET)), + wasmtime::Val(static_cast(spec.size()))} + ) + .unwrap(); + return f.state->get(result[0].i32()); +} + +TEST(StrModule, FormatStringWithSVerbPassesThrough) { + StrModuleFixture f; + const auto handle = f.state->create("hello"); + EXPECT_EQ(call_format_string(f, static_cast(handle), "s"), "hello"); + EXPECT_EQ(call_format_string(f, static_cast(handle), "10s"), " hello"); + EXPECT_EQ( + call_format_string(f, static_cast(handle), "-10s"), + "hello " + ); + EXPECT_EQ(call_format_string(f, static_cast(handle), ".3s"), "hel"); +} + +TEST(StrModule, FormatStringWithQVerbQuotesGoStyle) { + StrModuleFixture f; + const auto handle = f.state->create("hello"); + EXPECT_EQ(call_format_string(f, static_cast(handle), "q"), "\"hello\""); + + const auto h_special = f.state->create("a\nb\tc\"d\\e"); + EXPECT_EQ( + call_format_string(f, static_cast(h_special), "q"), + R"("a\nb\tc\"d\\e")" + ); + + const auto h_ctrl = f.state->create(std::string("\x01\x7F", 2)); + EXPECT_EQ( + call_format_string(f, static_cast(h_ctrl), "q"), + "\"\\x01\\x7f\"" + ); + + const auto h_empty = f.state->create(""); + EXPECT_EQ(call_format_string(f, static_cast(h_empty), "q"), "\"\""); +} + +TEST(StrModule, FormatStringWithXVerbHexEncodesBytes) { + StrModuleFixture f; + const auto handle = f.state->create("abc"); + EXPECT_EQ(call_format_string(f, static_cast(handle), "x"), "616263"); + EXPECT_EQ(call_format_string(f, static_cast(handle), "X"), "616263"); + const auto h_full = f.state->create(std::string("\xDE\xAD", 2)); + EXPECT_EQ(call_format_string(f, static_cast(h_full), "x"), "dead"); + EXPECT_EQ(call_format_string(f, static_cast(h_full), "X"), "DEAD"); +} + +TEST(FormatSpec, ParsesAllSpecComponents) { + EXPECT_EQ(parse_format_spec("d").verb, 'd'); + EXPECT_EQ(parse_format_spec("5d").width, 5); + EXPECT_EQ(parse_format_spec("-5d").minus, true); + EXPECT_EQ(parse_format_spec("05d").zero, true); + EXPECT_EQ(parse_format_spec("+d").plus, true); + EXPECT_EQ(parse_format_spec(" d").space, true); + EXPECT_EQ(parse_format_spec("#x").alt, true); + EXPECT_EQ(parse_format_spec(".2f").precision, 2); + EXPECT_EQ(parse_format_spec("10.2f").width, 10); + EXPECT_EQ(parse_format_spec("10.2f").precision, 2); + const auto full = parse_format_spec("+#-010.4X"); + EXPECT_TRUE(full.plus); + EXPECT_TRUE(full.alt); + EXPECT_TRUE(full.minus); + EXPECT_TRUE(full.zero); + EXPECT_EQ(full.width, 10); + EXPECT_EQ(full.precision, 4); + EXPECT_EQ(full.verb, 'X'); +} + +TEST(Utf8Encode, EncodesAscii) { + EXPECT_EQ(utf8_encode(0x41), "A"); + EXPECT_EQ(utf8_encode(0x7F), "\x7F"); +} + +TEST(Utf8Encode, EncodesMultiByteCodePoints) { + EXPECT_EQ(utf8_encode(0xF1), "\xC3\xB1"); // Γ± + EXPECT_EQ(utf8_encode(0x2603), "\xE2\x98\x83"); // β˜ƒ + EXPECT_EQ(utf8_encode(0x1F600), "\xF0\x9F\x98\x80"); // πŸ˜€ +} + +TEST(Utf8Encode, InvalidCodePointsBecomeReplacementChar) { + EXPECT_EQ(utf8_encode(-1), "\xEF\xBF\xBD"); + EXPECT_EQ(utf8_encode(0x110000), "\xEF\xBF\xBD"); + EXPECT_EQ(utf8_encode(0xD800), "\xEF\xBF\xBD"); +} + +TEST(GoQuote, MatchesGoStrconvQuoteForAscii) { + EXPECT_EQ(go_quote(""), "\"\""); + EXPECT_EQ(go_quote("hello"), "\"hello\""); + EXPECT_EQ(go_quote("a\"b"), "\"a\\\"b\""); + EXPECT_EQ(go_quote("a\\b"), "\"a\\\\b\""); + EXPECT_EQ(go_quote("a\nb"), "\"a\\nb\""); + EXPECT_EQ(go_quote("a\tb"), "\"a\\tb\""); + EXPECT_EQ(go_quote(std::string("\x00", 1)), "\"\\x00\""); + EXPECT_EQ(go_quote(std::string("\x1F", 1)), "\"\\x1f\""); + EXPECT_EQ(go_quote(std::string("\x7F", 1)), "\"\\x7f\""); +} } diff --git a/arc/go/analyzer/expression/expression.go b/arc/go/analyzer/expression/expression.go index 2a3cfee7d3..455c9b6f9b 100644 --- a/arc/go/analyzer/expression/expression.go +++ b/arc/go/analyzer/expression/expression.go @@ -524,7 +524,10 @@ func analyzePrimary(ctx context.Context[parser.IPrimaryExpressionContext]) { } return } - if ctx.AST.Literal() != nil { + if lit := ctx.AST.Literal(); lit != nil { + if strTerm := parser.StringTerminal(lit); strTerm != nil { + AnalyzeFmtStrLiteral(ctx, strTerm) + } return } if expr := ctx.AST.Expression(); expr != nil { diff --git a/arc/go/analyzer/expression/fmt_str.go b/arc/go/analyzer/expression/fmt_str.go new file mode 100644 index 0000000000..d0e6a73690 --- /dev/null +++ b/arc/go/analyzer/expression/fmt_str.go @@ -0,0 +1,110 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package expression + +import ( + "github.com/antlr4-go/antlr/v4" + "github.com/synnaxlabs/arc/analyzer/context" + "github.com/synnaxlabs/arc/analyzer/types" + "github.com/synnaxlabs/arc/literal" + "github.com/synnaxlabs/arc/parser" + basetypes "github.com/synnaxlabs/arc/types" + "github.com/synnaxlabs/x/diagnostics" + "github.com/synnaxlabs/x/errors" +) + +// AnalyzeFmtStrLiteral parses a format-string token (STR_LITERAL or +// STR_LITERAL_MULTI with f/rf prefix) and analyzes its placeholders. Body +// offsets map to source bytes so per-placeholder diagnostics anchor on the +// offending span. +func AnalyzeFmtStrLiteral[T antlr.ParserRuleContext]( + ctx context.Context[T], + strTerm antlr.TerminalNode, +) { + text := strTerm.GetText() + body, flags, ok := literal.StripQuotes(text) + if !ok { + ctx.Diagnostics.Add(diagnostics.Error( + errors.Newf("invalid string literal: %s", text), ctx.AST, + )) + return + } + if !flags.Format { + return + } + sym := strTerm.GetSymbol() + bodyOff := bodyOffset(text, flags) + base := diagnostics.Position{Line: sym.GetLine(), Col: sym.GetColumn() + bodyOff} + AnalyzeFmtStrSegments(ctx, body, base, ctx.AST) +} + +// bodyOffset returns the column offset from the start of a string token to the +// first byte of its body: the prefix length plus the one-character opening +// delimiter (either " or `). +func bodyOffset(text string, _ literal.StringFlags) int { + prefix := 0 + for prefix < 2 && prefix < len(text) && (text[prefix] == 'r' || text[prefix] == 'f') { + prefix++ + } + return prefix + 1 +} + +// AnalyzeFmtStrSegments parses body and analyzes each placeholder expression +// in ctx's scope. base is the source position of body[0]; placeholder +// diagnostics anchor on the offending `{...}` span. Returns parsed segments, +// or nil if body is malformed. +func AnalyzeFmtStrSegments[T antlr.ParserRuleContext]( + ctx context.Context[T], + body string, + base diagnostics.Position, + anchor antlr.ParserRuleContext, +) []literal.FmtStrSegment { + segments, err := literal.FmtStrParse(body) + if err != nil { + ctx.Diagnostics.Add(diagnostics.Error(err, anchor)) + return nil + } + for _, seg := range segments { + if !seg.IsPlaceholder { + continue + } + segStart := base.Advance(body, seg.Start) + segEnd := base.Advance(body, seg.End) + emit := func(d diagnostics.Diagnostic) { + ctx.Diagnostics.Add(d.WithRange(segStart, segEnd)) + } + if seg.Text == "" { + emit(diagnostics.Errorf(anchor, + "placeholder '{}' must contain an expression")) + continue + } + expr, diags := parser.ParseExpression(seg.Text) + if diags != nil && !diags.Ok() { + emit(diagnostics.Errorf(anchor, + "invalid placeholder expression %q: %s", seg.Text, diags.String())) + continue + } + Analyze(context.Child(ctx, expr)) + t := types.InferFromExpression(context.Child(ctx, expr)).UnwrapChan() + if !t.IsNumeric() && t.Kind != basetypes.KindString { + emit(diagnostics.Errorf(anchor, + "placeholder %q has type %s; only numeric and string types are supported", + seg.Text, t)) + continue + } + if seg.Spec == "" { + continue + } + if err := literal.FmtStrValidateSpec(seg.Spec, t); err != nil { + emit(diagnostics.Error(err, anchor)) + } + } + return segments +} diff --git a/arc/go/analyzer/expression/fmt_str_test.go b/arc/go/analyzer/expression/fmt_str_test.go new file mode 100644 index 0000000000..36ee4fbf9d --- /dev/null +++ b/arc/go/analyzer/expression/fmt_str_test.go @@ -0,0 +1,363 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package expression_test + +import ( + "fmt" + "strings" + + "github.com/antlr4-go/antlr/v4" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/synnaxlabs/arc/analyzer" + acontext "github.com/synnaxlabs/arc/analyzer/context" + "github.com/synnaxlabs/arc/analyzer/expression" + "github.com/synnaxlabs/arc/parser" + "github.com/synnaxlabs/arc/symbol" + "github.com/synnaxlabs/arc/types" + "github.com/synnaxlabs/x/diagnostics" + . "github.com/synnaxlabs/x/testutil" +) + +var _ = Describe("Format String Analyzer Diagnostics", func() { + fmtResolver := func() symbol.MapResolver { + return symbol.MapResolver{ + "chI32": {Name: "chI32", Kind: symbol.KindChannel, Type: types.Chan(types.I32())}, + "chF64": {Name: "chF64", Kind: symbol.KindChannel, Type: types.Chan(types.F64())}, + "chStr": {Name: "chStr", Kind: symbol.KindChannel, Type: types.Chan(types.String())}, + "chU8": {Name: "chU8", Kind: symbol.KindChannel, Type: types.Chan(types.U8())}, + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8())}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String())}, + } + } + + analyze := func(specCtx SpecContext, code string) diagnostics.Diagnostics { + ast := MustSucceed(parser.Parse(code)) + ctx := acontext.CreateRoot(specCtx, ast, fmtResolver()) + analyzer.AnalyzeProgram(ctx) + return *ctx.Diagnostics + } + + findError := func(diags diagnostics.Diagnostics, substr string) *diagnostics.Diagnostic { + for i := range diags { + d := diags[i] + if d.Severity == diagnostics.SeverityError && strings.Contains(d.Message, substr) { + return &d + } + } + return nil + } + + countErrors := func(diags diagnostics.Diagnostics, substr string) int { + n := 0 + for _, d := range diags { + if d.Severity == diagnostics.SeverityError && strings.Contains(d.Message, substr) { + n++ + } + } + return n + } + + expectError := func(specCtx SpecContext, code, substr string) diagnostics.Diagnostic { + diags := analyze(specCtx, code) + Expect(diags.Ok()).To(BeFalse(), + fmt.Sprintf("expected an error matching %q but analysis succeeded", substr)) + got := findError(diags, substr) + Expect(got).ToNot(BeNil(), + fmt.Sprintf("no error matched %q; got:\n%s", substr, diags.String())) + return *got + } + + expectSuccess := func(specCtx SpecContext, code string) { + diags := analyze(specCtx, code) + Expect(diags.Ok()).To(BeTrue(), diags.String()) + } + + wrap := func(body string) string { + return `func f() { +` + body + ` +} +trig -> f{}` + } + + Describe("Body parse errors (literal.FmtStrParse)", func() { + DescribeTable("rejects malformed format string bodies", + func(specCtx SpecContext, body, errSubstr string) { + expectError(specCtx, wrap(` log = `+body), errSubstr) + }, + Entry("unmatched opening brace at end", `f"{x"`, "unmatched '{'"), + Entry("unmatched opening brace mid-text", `f"pre {x more"`, "unmatched '{'"), + Entry("nested unmatched open inside placeholder", `f"{x{y}"`, "unmatched"), + Entry("empty placeholder body", `f"{}"`, "must contain an expression"), + Entry("empty spec after colon", `f"{chI32:}"`, "format spec after ':' is empty"), + Entry("empty expression before colon", `f"{:d}"`, "must contain an expression before ':'"), + ) + }) + + Describe("Placeholder expression parse errors (parser.ParseExpression)", func() { + DescribeTable("rejects unparseable placeholder bodies", + func(specCtx SpecContext, body, errSubstr string) { + expectError(specCtx, wrap(` log = `+body), errSubstr) + }, + Entry("trailing operator", `f"{chI32 +}"`, "invalid placeholder expression"), + Entry("leading operator", `f"{* chI32}"`, "invalid placeholder expression"), + Entry("unclosed paren in expression", `f"{(chI32}"`, "invalid placeholder expression"), + ) + }) + + Describe("Placeholder type checks", func() { + It("accepts a numeric (i32) placeholder", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`f"{chI32}"`)) + }) + + It("accepts a string-typed placeholder", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`f"{chStr}"`)) + }) + + It("accepts a numeric literal placeholder", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`f"{42}"`)) + }) + + It("rejects a placeholder referencing an undeclared identifier", func(specCtx SpecContext) { + diags := analyze(specCtx, wrap(` log = `+`f"{undeclared}"`)) + Expect(diags.Ok()).To(BeFalse(), + "expected an error for undeclared identifier") + }) + + It("anchors the placeholder type diagnostic on the {...} span for an undeclared identifier", func(specCtx SpecContext) { + // Source line 2: ` log = f"pre {undeclared} post"` + // '{' sits at col 16 and '}' at col 27 in the analyzer's column scheme, + // so the placeholder span runs [16, 28). + code := "func f() {\n log = f\"pre {undeclared} post\"\n}\ntrig -> f{}" + diags := analyze(specCtx, code) + Expect(diags.Ok()).To(BeFalse()) + d := findError(diags, `placeholder "undeclared"`) + Expect(d).ToNot(BeNil(), + "expected a placeholder type diagnostic naming \"undeclared\"; got:\n%s", diags.String()) + Expect(d.Start.Line).To(Equal(2)) + Expect(d.End.Line).To(Equal(2)) + Expect(d.Start.Col).To(Equal(16), "span should start at the '{' column") + Expect(d.End.Col).To(Equal(28), "span should end one past the '}' column") + }) + }) + + Describe("Format spec validation", func() { + DescribeTable("rejects float-only specs on integer placeholders", + func(specCtx SpecContext, body string) { + expectError(specCtx, wrap(` log = `+body), "invalid format spec") + }, + Entry("i32 channel :f", `f"{chI32:f}"`), + Entry("i32 channel :.2f", `f"{chI32:.2f}"`), + Entry("i32 channel :e", `f"{chI32:e}"`), + Entry("i32 channel :g", `f"{chI32:g}"`), + ) + + DescribeTable("rejects integer-only specs on float placeholders", + func(specCtx SpecContext, body string) { + expectError(specCtx, wrap(` log = `+body), "invalid format spec") + }, + Entry("f64 channel :d", `f"{chF64:d}"`), + Entry("f64 channel :o", `f"{chF64:o}"`), + ) + + DescribeTable("rejects invalid verbs on string placeholders", + func(specCtx SpecContext, body string) { + expectError(specCtx, wrap(` log = `+body), "invalid format spec") + }, + Entry("string channel :d", `f"{chStr:d}"`), + Entry("string channel :.2f", `f"{chStr:.2f}"`), + ) + + DescribeTable("rejects blacklisted verbs across placeholder types", + func(specCtx SpecContext, body string) { + expectError(specCtx, wrap(` log = `+body), "invalid format spec") + }, + Entry("i32 channel :T", `f"{chI32:T}"`), + Entry("f64 channel :T", `f"{chF64:T}"`), + Entry("string channel :T", `f"{chStr:T}"`), + Entry("i32 channel :v", `f"{chI32:v}"`), + Entry("f64 channel :v", `f"{chF64:v}"`), + Entry("string channel :v", `f"{chStr:v}"`), + Entry("integer literal :T", `f"{42:T}"`), + Entry("i32 channel :U", `f"{chI32:U}"`), + Entry("u8 channel :U", `f"{chU8:U}"`), + Entry("integer literal :U", `f"{42:U}"`), + Entry("string channel :x", `f"{chStr:x}"`), + Entry("string channel :X", `f"{chStr:X}"`), + ) + + DescribeTable("accepts valid specs", + func(specCtx SpecContext, body string) { + expectSuccess(specCtx, wrap(` log = `+body)) + }, + Entry("i32 channel :d", `f"{chI32:d}"`), + Entry("i32 channel :05d", `f"{chI32:05d}"`), + Entry("i32 channel :x", `f"{chI32:x}"`), + Entry("f64 channel :.2f", `f"{chF64:.2f}"`), + Entry("f64 channel :e", `f"{chF64:e}"`), + Entry("u8 channel :d", `f"{chU8:d}"`), + Entry("string channel :s", `f"{chStr:s}"`), + Entry("string channel :q", `f"{chStr:q}"`), + Entry("integer literal :d", `f"{123:d}"`), + Entry("float literal :.2f", `f"{3.14:.2f}"`), + ) + + }) + + Describe("Multiple ':' in placeholder (last ':' is the spec separator)", func() { + It("treats the rightmost ':' in `{x:y:d}` as the spec separator (i32)", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` x i32 := 10 + y i32 := 3 + log = `+`f"{x:y:d}"`)) + }) + + It("treats the rightmost ':' in `{x:y:.2f}` as the spec separator (f64)", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` x f64 := 1.0 + y f64 := 0.5 + log = `+`f"{x:y:.2f}"`)) + }) + + It("treats the rightmost ':' in three-':' bodies as the spec separator", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` a i32 := 7 + b i32 := 3 + c i32 := 2 + log = `+`f"{a:b:c:d}"`)) + }) + + It("rejects when the rightmost ':' produces an invalid spec, regardless of earlier ':'", func(specCtx SpecContext) { + expectError(specCtx, wrap(` x i32 := 10 + y i32 := 3 + log = `+`f"{x:y:z}"`), "invalid format spec") + }) + + It("rejects when the rightmost ':' splits a spec invalid for the resulting expression type", func(specCtx SpecContext) { + expectError(specCtx, wrap(` x i32 := 10 + y i32 := 3 + log = `+`f"{x:y:f}"`), "invalid format spec") + }) + + It("with `{x:y}` (single ':' between two i32 vars) splits y as the spec", func(specCtx SpecContext) { + expectError(specCtx, wrap(` x i32 := 10 + y i32 := 3 + log = `+`f"{x:y}"`), `invalid format spec "y"`) + }) + }) + + Describe("Empty body and trivial cases", func() { + It("accepts an empty format string (no placeholders)", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`f""`)) + }) + + It("accepts a literal-only format string", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`f"hello world"`)) + }) + + It("accepts a doubled open brace and bare close with no placeholder", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`f"{{ }"`)) + }) + + It("accepts an rf-prefixed format string with a placeholder", func(specCtx SpecContext) { + expectSuccess(specCtx, wrap(` log = `+`rf"v={chI32}"`)) + }) + + It("rejects an invalid spec inside an rf-prefixed format string", func(specCtx SpecContext) { + expectError(specCtx, wrap(` log = `+`rf"v={chStr:d}"`), "invalid format spec") + }) + + It("accepts an rf-prefixed multi-line format string with placeholders across newlines", func(specCtx SpecContext) { + code := "func f() {\n log = rf`v={chI32}\nt={chF64}`\n}\ntrig -> f{}" + expectSuccess(specCtx, code) + }) + }) + + Describe("Diagnostic position anchoring", func() { + It("anchors a placeholder spec error on the same line as the literal", func(specCtx SpecContext) { + code := "func f() {\n log = f\"{chStr:d}\"\n}\ntrig -> f{}" + d := expectError(specCtx, code, "invalid format spec") + Expect(d.Start.Line).To(Equal(2), + "expected diagnostic on line 2, got line %d (col %d)", d.Start.Line, d.Start.Col) + Expect(d.End.Line).To(BeNumerically(">=", d.Start.Line)) + if d.End.Line == d.Start.Line { + Expect(d.End.Col).To(BeNumerically(">", d.Start.Col), + "expected nonzero placeholder span") + } + }) + + It("anchors a placeholder spec error on a later line for a multi-line format string", func(specCtx SpecContext) { + code := "func f() {\n log = f`line1\nline2\n{chStr:d}`\n}\ntrig -> f{}" + d := expectError(specCtx, code, "invalid format spec") + Expect(d.Start.Line).To(Equal(4), + "expected diagnostic on line 4 (third line of literal), got line %d col %d", + d.Start.Line, d.Start.Col) + }) + + It("anchors a placeholder error past the opening quote on a single-line literal", func(specCtx SpecContext) { + code := "func f() {\n log = f\"pre {chStr:d} post\"\n}\ntrig -> f{}" + d := expectError(specCtx, code, "invalid format spec") + Expect(d.Start.Line).To(Equal(2)) + Expect(d.Start.Col).To(BeNumerically(">", 11), + "placeholder column %d should be past the opening quote", d.Start.Col) + }) + }) + + Describe("Defensive guards", func() { + // findStringTerminal locates the first STR_LITERAL or STR_LITERAL_MULTI + // terminal in tree. + var findStringTerminal func(t antlr.Tree) antlr.TerminalNode + findStringTerminal = func(t antlr.Tree) antlr.TerminalNode { + if tn, ok := t.(antlr.TerminalNode); ok { + if tok := tn.GetSymbol(); tok != nil { + tt := tok.GetTokenType() + if tt == parser.ArcParserSTR_LITERAL || tt == parser.ArcParserSTR_LITERAL_MULTI { + return tn + } + } + } + for i := 0; i < t.GetChildCount(); i++ { + if found := findStringTerminal(t.GetChild(i)); found != nil { + return found + } + } + return nil + } + + It("emits a diagnostic when string token text lacks delimiters", func(specCtx SpecContext) { + ast := MustSucceed(parser.Parse("func f() {\n log = f\"x\"\n}\ntrig -> f{}")) + strTerm := findStringTerminal(ast) + Expect(strTerm).ToNot(BeNil()) + // Mutate token text to drop the delimiters. This is unreachable via + // the grammar but exercises the StripQuotes guard. + strTerm.GetSymbol().SetText("no_quotes") + ctx := acontext.CreateRoot(specCtx, ast, fmtResolver()) + expression.AnalyzeFmtStrLiteral(ctx, strTerm) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect(findError(*ctx.Diagnostics, "invalid string literal")). + ToNot(BeNil()) + }) + }) + + Describe("Multiple errors in one literal", func() { + It("emits one diagnostic per offending placeholder", func(specCtx SpecContext) { + code := wrap(` log = ` + `f"{chStr:d} {chF64:d}"`) + diags := analyze(specCtx, code) + Expect(diags.Ok()).To(BeFalse()) + Expect(countErrors(diags, "invalid format spec")).To(Equal(2), + "expected one diagnostic per placeholder; got:\n%s", diags.String()) + }) + + It("continues analyzing later placeholders after an earlier spec error", func(specCtx SpecContext) { + code := wrap(` log = ` + `f"{chStr:d} and {chF64:d}"`) + diags := analyze(specCtx, code) + Expect(diags.Ok()).To(BeFalse()) + Expect(countErrors(diags, "invalid format spec")).To(Equal(2), + "expected later placeholder error; got:\n%s", diags.String()) + }) + }) +}) diff --git a/arc/go/analyzer/expression/typecast_test.go b/arc/go/analyzer/expression/typecast_test.go index f055bc43a6..2aa80dcff0 100644 --- a/arc/go/analyzer/expression/typecast_test.go +++ b/arc/go/analyzer/expression/typecast_test.go @@ -373,6 +373,16 @@ var _ = Describe("Type Casts", func() { y := str(-3.14) } `), + Entry("float literal to str (negative zero)", ` + func testFunc() { + y := str(-0.0) + } + `), + Entry("float literal to str (negative zero with trailing zeros)", ` + func testFunc() { + y := str(-0.0000) + } + `), Entry("float literal to str (trailing zero)", ` func testFunc() { y := str(1.0) diff --git a/arc/go/analyzer/flow/expression.go b/arc/go/analyzer/flow/expression.go index 823eadcc28..0147e7b1be 100644 --- a/arc/go/analyzer/flow/expression.go +++ b/arc/go/analyzer/flow/expression.go @@ -14,22 +14,50 @@ import ( "github.com/synnaxlabs/arc/analyzer/expression" atypes "github.com/synnaxlabs/arc/analyzer/types" "github.com/synnaxlabs/arc/ir" + "github.com/synnaxlabs/arc/literal" "github.com/synnaxlabs/arc/parser" "github.com/synnaxlabs/arc/symbol" "github.com/synnaxlabs/arc/types" "github.com/synnaxlabs/x/diagnostics" ) -// AnalyzeSingleExpression converts an inline expression into a synthetic function that -// can be used as a node in a flow graph. Pure literals are registered as KindConstant -// symbols and don't require code compilation. +// AnalyzeSingleExpression converts an inline expression into a synthetic function +// node. Format-string placeholders are analyzed here; IR shape is chosen downstream. func AnalyzeSingleExpression(ctx acontext.Context[parser.IExpressionContext]) { exprType := atypes.InferFromExpression(ctx).Unwrap() t := types.Function(types.FunctionProperties{}) t.Outputs = append(t.Outputs, types.Param{Name: ir.DefaultOutputParam, Type: exprType}) - // Pure literals become constants - no code to compile + // Literals register as KindConstant; format strings with placeholders register + // as synthetic functions so placeholder channel reads track on the right symbol. if parser.IsLiteral(ctx.AST) { + if lit := parser.GetLiteral(ctx.AST); lit != nil { + if strTerm := parser.StringTerminal(lit); strTerm != nil { + body, flags, ok := literal.StripQuotes(strTerm.GetText()) + if !ok { + ctx.Diagnostics.Add(diagnostics.Errorf(ctx.AST, + "invalid string literal: %s", strTerm.GetText())) + return + } + if flags.Format { + segs, err := literal.FmtStrParse(body) + if err != nil { + ctx.Diagnostics.Add(diagnostics.Error(err, ctx.AST)) + return + } + if literal.FmtStrHasPlaceholder(segs) { + fnScope, err := ctx.Scope.Root().Add(ctx, symbol.Symbol{Kind: symbol.KindFunction, Type: t, AST: ctx.AST}) + if err != nil { + ctx.Diagnostics.Add(diagnostics.Error(err, ctx.AST)) + return + } + fnScope.AutoName("fmt_str_") + expression.AnalyzeFmtStrLiteral(ctx.WithScope(fnScope), strTerm) + return + } + } + } + } t.Config = append(t.Config, types.Param{Name: "value", Type: exprType}) scope, err := ctx.Scope.Root().Add(ctx, symbol.Symbol{ Kind: symbol.KindConstant, diff --git a/arc/go/analyzer/flow/expression_test.go b/arc/go/analyzer/flow/expression_test.go index cfa9a8d649..060e22f2c4 100644 --- a/arc/go/analyzer/flow/expression_test.go +++ b/arc/go/analyzer/flow/expression_test.go @@ -202,6 +202,113 @@ var _ = Describe("AnalyzeSingleExpression", func() { }) }) + Describe("Raw String Format Literals", func() { + It("should create KindConstant for raw string without placeholders", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"static"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + constSym := MustSucceed(ctx.Scope.Resolve(ctx, "constant_0")) + Expect(constSym.Kind).To(Equal(symbol.KindConstant)) + }) + + It("should create fmt_str synthetic function for raw string with placeholder", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"v={ox_pt_1}"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + fnSym := MustSucceed(ctx.Scope.Resolve(ctx, "fmt_str_0")) + Expect(fnSym.Kind).To(Equal(symbol.KindFunction)) + Expect(fnSym.Type.Kind).To(Equal(types.KindFunction)) + output := MustBeOk(fnSym.Type.Outputs.Get(ir.DefaultOutputParam)) + Expect(output.Type).To(Equal(types.String())) + }) + + It("should track placeholder channel reads on the synthetic function", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"v={ox_pt_1}"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + fnSym := MustSucceed(ctx.Scope.Resolve(ctx, "fmt_str_0")) + Expect(fnSym.Channels.Read).To(HaveLen(1)) + Expect(fnSym.Channels.Read[12]).To(Equal("ox_pt_1")) + }) + + It("should track multiple placeholder channel reads", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"a={ox_pt_1} b={ox_pt_2}"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + fnSym := MustSucceed(ctx.Scope.Resolve(ctx, "fmt_str_0")) + Expect(fnSym.Channels.Read).To(HaveLen(2)) + Expect(fnSym.Channels.Read[12]).To(Equal("ox_pt_1")) + Expect(fnSym.Channels.Read[13]).To(Equal("ox_pt_2")) + }) + + It("should accept a numeric literal placeholder with format spec", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"x={42%05d}"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + fnSym := MustSucceed(ctx.Scope.Resolve(ctx, "fmt_str_0")) + Expect(fnSym.Kind).To(Equal(symbol.KindFunction)) + }) + + It("should auto-increment fmt_str names across multiple raw strings", func(bCtx SpecContext) { + expr0 := MustSucceed(parser.ParseExpression(`f"{ox_pt_1}"`)) + ctx := context.CreateRoot(bCtx, expr0, testResolver) + flow.AnalyzeSingleExpression(ctx) + + expr1 := MustSucceed(parser.ParseExpression(`f"{ox_pt_2}"`)) + ctx1 := context.Context[parser.IExpressionContext]{ + Context: bCtx, + Scope: ctx.Scope, + Diagnostics: ctx.Diagnostics, + Constraints: ctx.Constraints, + TypeMap: ctx.TypeMap, + AST: expr1, + } + flow.AnalyzeSingleExpression(ctx1) + + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + MustSucceed(ctx.Scope.Resolve(ctx, "fmt_str_0")) + MustSucceed(ctx.Scope.Resolve(ctx, "fmt_str_1")) + }) + + It("should report unmatched opening brace in raw string body", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"{x"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("unmatched")) + }) + + It("should report empty placeholder", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"pre {} post"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("must contain an expression")) + }) + + It("should report undefined identifier in placeholder", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"x={unknown_ch}"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("undefined symbol")) + }) + + It("should report invalid format spec for placeholder type", func(bCtx SpecContext) { + expr := MustSucceed(parser.ParseExpression(`f"x={ox_pt_1:s}"`)) + ctx := context.CreateRoot(bCtx, expr, testResolver) + flow.AnalyzeSingleExpression(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("invalid format spec")) + }) + + }) + Describe("Error Cases", func() { It("should report undefined symbol in expression", func(bCtx SpecContext) { expr := MustSucceed(parser.ParseExpression(`unknown_channel > 100`)) diff --git a/arc/go/analyzer/flow/flow.go b/arc/go/analyzer/flow/flow.go index 2ccae24976..03264d32a2 100644 --- a/arc/go/analyzer/flow/flow.go +++ b/arc/go/analyzer/flow/flow.go @@ -92,6 +92,10 @@ func parseFunction(ctx context.Context[parser.IFunctionContext], prevNode parser return } + // Dual-shape ExecBoth (see symbol.ExecBoth): upstream is a trigger, not + // a typed input. + upstreamIsTrigger := funcType.Exec == symbol.ExecBoth && len(funcType.Type.Config) > 0 + if prevIDNode := prevNode.Identifier(); prevIDNode != nil { idName := prevIDNode.IDENTIFIER().GetText() idSym, err := ctx.Resolve(idName) @@ -104,7 +108,7 @@ func parseFunction(ctx context.Context[parser.IFunctionContext], prevNode parser ctx.Diagnostics.Add(diagnostics.Errorf(prevIDNode, "%s is not a channel", idName)) return } - if len(funcType.Type.Inputs) > 0 { + if !upstreamIsTrigger && len(funcType.Type.Inputs) > 0 { param := funcType.Type.Inputs[0] if idSym.Type.Kind != types.KindChan { ctx.Diagnostics.Add(diagnostics.Errorf(ctx.AST, @@ -133,7 +137,7 @@ func parseFunction(ctx context.Context[parser.IFunctionContext], prevNode parser } } else if prevExpr := prevNode.Expression(); prevExpr != nil { exprType := atypes.InferFromExpression(context.Child(ctx, prevExpr)).Unwrap() - if len(funcType.Type.Inputs) > 0 { + if !upstreamIsTrigger && len(funcType.Type.Inputs) > 0 { param := funcType.Type.Inputs[0] if err := atypes.Check( ctx.Constraints, @@ -168,11 +172,11 @@ func parseFunction(ctx context.Context[parser.IFunctionContext], prevNode parser } } - if !hasRoutingTableBetween && len(funcType.Type.Inputs) > 1 { + if !upstreamIsTrigger && !hasRoutingTableBetween && len(funcType.Type.Inputs) > 1 { ctx.Diagnostics.Add(diagnostics.Errorf(ctx.AST, "%s has more than one parameter", name)) return } - if !hasRoutingTableBetween && len(funcType.Type.Inputs) > 0 { + if !upstreamIsTrigger && !hasRoutingTableBetween && len(funcType.Type.Inputs) > 0 { t := funcType.Type.Inputs[0].Type var prevOutputType types.Type if outputType, ok := prevFuncType.Type.Outputs.Get(ir.DefaultOutputParam); ok { diff --git a/arc/go/analyzer/flow/flow_test.go b/arc/go/analyzer/flow/flow_test.go index b26e08adbf..9d7abef73d 100644 --- a/arc/go/analyzer/flow/flow_test.go +++ b/arc/go/analyzer/flow/flow_test.go @@ -1269,3 +1269,199 @@ sequence main { }) }) }) + +// execBothFn builds an ExecBoth function symbol with Inputs mirroring Config +// one-for-one, matching the shape required by the symbol.ExecBoth contract. +func execBothFn(name string, params types.Params, output types.Type) symbol.Symbol { + return symbol.Symbol{ + Name: name, + Kind: symbol.KindFunction, + Exec: symbol.ExecBoth, + Type: types.Function(types.FunctionProperties{ + Inputs: params, + Config: params, + Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: output}}, + }), + } +} + +func execFlowFn(name string, inputs types.Params, output types.Type) symbol.Symbol { + props := types.FunctionProperties{Inputs: inputs} + if output.Kind != types.KindInvalid { + props.Outputs = types.Params{{Name: ir.DefaultOutputParam, Type: output}} + } + return symbol.Symbol{ + Name: name, + Kind: symbol.KindFunction, + Exec: symbol.ExecFlow, + Type: types.Function(types.FunctionProperties{ + Inputs: props.Inputs, + Outputs: props.Outputs, + }), + } +} + +var _ = Describe("upstreamIsTrigger Suppression", func() { + Describe("ExecBoth with non-empty Config (suppression active)", func() { + It("Should accept channel upstream whose value type does not match the input", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.F64()), ID: 1}, + "x": {Name: "x", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 2}, + "fmtfn": execBothFn( + "fmtfn", + types.Params{{Name: "x", Type: types.Chan(types.I32())}}, + types.String(), + ), + } + ast := MustSucceed(parser.Parse(`trig -> fmtfn{x=x}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + }) + + It("Should accept expression upstream whose type does not match the input", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.F64()), ID: 1}, + "x": {Name: "x", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 2}, + "fmtfn": execBothFn( + "fmtfn", + types.Params{{Name: "x", Type: types.Chan(types.I32())}}, + types.String(), + ), + } + ast := MustSucceed(parser.Parse(`trig > 100.0 -> fmtfn{x=x}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + }) + + It("Should accept upstream function whose output type does not match the input", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "x": {Name: "x", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 1}, + "producer": execFlowFn("producer", nil, types.U8()), + "fmtfn": execBothFn( + "fmtfn", + types.Params{{Name: "x", Type: types.Chan(types.I32())}}, + types.String(), + ), + } + ast := MustSucceed(parser.Parse(`producer{} -> fmtfn{x=x}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + }) + + It("Should accept upstream function feeding an ExecBoth fn with multiple inputs", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "x": {Name: "x", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 1}, + "y": {Name: "y", Kind: symbol.KindChannel, Type: types.Chan(types.F64()), ID: 2}, + "producer": execFlowFn("producer", nil, types.F64()), + "fmtfn": execBothFn( + "fmtfn", + types.Params{ + {Name: "x", Type: types.Chan(types.I32())}, + {Name: "y", Type: types.Chan(types.F64())}, + }, + types.String(), + ), + } + ast := MustSucceed(parser.Parse(`producer{} -> fmtfn{x=x, y=y}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + }) + + It("Should accept void upstream function feeding an ExecBoth fn with inputs", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "x": {Name: "x", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 1}, + "void": execFlowFn("void", nil, types.Type{}), + "fmtfn": execBothFn( + "fmtfn", + types.Params{{Name: "x", Type: types.Chan(types.I32())}}, + types.String(), + ), + } + ast := MustSucceed(parser.Parse(`void{} -> fmtfn{x=x}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + }) + }) + + Describe("Non-ExecBoth (regression guards: suppression must NOT activate)", func() { + It("Should reject channel upstream with mismatched value type for an ExecFlow fn", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F64()), ID: 1}, + "sink": execFlowFn("sink", types.Params{{Name: "v", Type: types.I32()}}, types.Type{}), + } + ast := MustSucceed(parser.Parse(`sensor -> sink{}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("does not match")) + }) + + It("Should reject expression upstream with mismatched type for an ExecFlow fn", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F64()), ID: 1}, + "sink": execFlowFn("sink", types.Params{{Name: "v", Type: types.String()}}, types.Type{}), + } + ast := MustSucceed(parser.Parse(`sensor > 100.0 -> sink{}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("does not match")) + }) + + It("Should reject upstream function feeding an ExecFlow fn with multiple inputs", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "producer": execFlowFn("producer", nil, types.F64()), + "multi": execFlowFn( + "multi", + types.Params{ + {Name: "a", Type: types.F64()}, + {Name: "b", Type: types.F64()}, + }, + types.Type{}, + ), + } + ast := MustSucceed(parser.Parse(`producer{} -> multi{}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(Equal("multi has more than one parameter")) + }) + + It("Should reject upstream function output type mismatch for an ExecFlow fn", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "producer": execFlowFn("producer", nil, types.U8()), + "sink": execFlowFn("sink", types.Params{{Name: "v", Type: types.F64()}}, types.Type{}), + } + ast := MustSucceed(parser.Parse(`producer{} -> sink{}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeFalse()) + Expect((*ctx.Diagnostics)[0].Message).To(ContainSubstring("is not equal to argument type")) + }) + }) + + Describe("ExecBoth with empty Config (suppression NOT active)", func() { + It("Should treat an ExecBoth fn with empty Config like a normal flow fn", func(bCtx SpecContext) { + r := symbol.MapResolver{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 1}, + "now": { + Name: "now", + Kind: symbol.KindFunction, + Exec: symbol.ExecBoth, + Type: types.Function(types.FunctionProperties{ + Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.TimeStamp()}}, + }), + }, + } + ast := MustSucceed(parser.Parse(`trig -> now{}`)) + ctx := context.CreateRoot(bCtx, ast, r) + analyzer.AnalyzeProgram(ctx) + Expect(ctx.Diagnostics.Ok()).To(BeTrue(), ctx.Diagnostics.String()) + }) + }) +}) diff --git a/arc/go/analyzer/types/infer_expression.go b/arc/go/analyzer/types/infer_expression.go index c3fb6e7480..c364704bd9 100644 --- a/arc/go/analyzer/types/infer_expression.go +++ b/arc/go/analyzer/types/infer_expression.go @@ -280,7 +280,7 @@ func inferLiteralType(ctx context.Context[parser.ILiteralContext]) types.Type { if seriesLit := ctx.AST.SeriesLiteral(); seriesLit != nil { return inferSeriesLiteralType(context.Child(ctx, seriesLit)) } - if ctx.AST.STR_LITERAL() != nil || ctx.AST.STR_LITERAL_RAW() != nil { + if parser.StringTerminal(ctx.AST) != nil { t := types.String() ctx.TypeMap[ctx.AST] = t return t diff --git a/arc/go/analyzer/types/infer_test.go b/arc/go/analyzer/types/infer_test.go index 6b37e23d87..bec5419de1 100644 --- a/arc/go/analyzer/types/infer_test.go +++ b/arc/go/analyzer/types/infer_test.go @@ -114,10 +114,10 @@ var _ = Describe("Type Inference", func() { Entry("integer literal", "42", types.KindVariable, types.KindIntegerConstant), Entry("float literal", "3.14", types.KindVariable, types.KindFloatConstant), Entry("string literal", `"hello"`, types.KindString, types.KindInvalid), - Entry("raw string literal", "`hello`", types.KindString, types.KindInvalid), - Entry("empty raw string literal", "``", types.KindString, types.KindInvalid), - Entry("multi-line raw string literal", "`a\nb`", types.KindString, types.KindInvalid), - Entry("raw string with escaped backtick", "`say \\`hi\\``", types.KindString, types.KindInvalid), + Entry("raw string literal", `f"hello"`, types.KindString, types.KindInvalid), + Entry("empty raw string literal", `f""`, types.KindString, types.KindInvalid), + Entry("multi-line raw string literal", `f"a\nb"`, types.KindString, types.KindInvalid), + Entry("raw string literal", `r"say \"hi\""`, types.KindString, types.KindInvalid), Entry("boolean true", "true", types.KindU8, types.KindInvalid), Entry("boolean false", "false", types.KindU8, types.KindInvalid), ) diff --git a/arc/go/compiler/compiler.go b/arc/go/compiler/compiler.go index b9af6a71db..2c602e72b4 100644 --- a/arc/go/compiler/compiler.go +++ b/arc/go/compiler/compiler.go @@ -33,6 +33,7 @@ package compiler import ( "context" "slices" + "strings" "github.com/antlr4-go/antlr/v4" ccontext "github.com/synnaxlabs/arc/compiler/context" @@ -41,12 +42,15 @@ import ( "github.com/synnaxlabs/arc/compiler/statement" "github.com/synnaxlabs/arc/compiler/wasm" "github.com/synnaxlabs/arc/ir" + "github.com/synnaxlabs/arc/literal" "github.com/synnaxlabs/arc/parser" "github.com/synnaxlabs/arc/symbol" "github.com/synnaxlabs/arc/types" "github.com/synnaxlabs/x/errors" ) +const FmtStrSyntheticPrefix = "fmt$" + type compiledFunction struct { scopeName string typeIdx uint32 @@ -94,6 +98,14 @@ func Compile(ctx context.Context, program ir.IR, opts ...Option) (Output, error) var compiled []compiledFunction for _, i := range program.Functions { + if strings.HasPrefix(i.Key, FmtStrSyntheticPrefix) { + cf, err := compileFmtStrSynthetic(compCtx, i) + if err != nil { + return Output{}, err + } + compiled = append(compiled, cf) + continue + } params := slices.Concat(i.Config, i.Inputs) var returnType types.Type defaultOutput, hasDefaultOutput := i.Outputs.Get(ir.DefaultOutputParam) @@ -203,6 +215,31 @@ func compileExpression(ctx ccontext.Context[parser.IExpressionContext]) error { return err } +// compileFmtStrSynthetic emits a zero-param WASM body returning the +// formatted string handle for an analyzer-synthesized backtick Function. +func compileFmtStrSynthetic( + rootCtx ccontext.Context[antlr.ParserRuleContext], + fn ir.Function, +) (compiledFunction, error) { + segments, err := literal.FmtStrParse(fn.Body.Raw) + if err != nil { + return compiledFunction{}, err + } + ctx := rootCtx.WithNewWriter() + funcT := wasm.FunctionType{ + Results: []wasm.ValueType{wasm.ConvertType(types.String())}, + } + typeIdx := ctx.Module.AddType(funcT) + if _, err := expression.EmitFmtSegments(ctx, segments); err != nil { + return compiledFunction{}, err + } + return compiledFunction{ + scopeName: fn.Key, + typeIdx: typeIdx, + writer: ctx.Writer, + }, nil +} + func collectLocals(scope *symbol.Scope) []wasm.ValueType { var locals []wasm.ValueType for _, child := range scope.Children { diff --git a/arc/go/compiler/compiler_test.go b/arc/go/compiler/compiler_test.go index 4ac47219bd..3997158b79 100644 --- a/arc/go/compiler/compiler_test.go +++ b/arc/go/compiler/compiler_test.go @@ -3132,6 +3132,117 @@ var _ = Describe("Compiler", func() { }) }) + Describe("Format String Synthetic Functions", func() { + var strMod *stlstrings.Module + var strState *stlstrings.ProgramState + + BeforeEach(func(ctx SpecContext) { + _, strMod, strState = bindDefaultModules(ctx, r) + }) + + It("Compiles a flow-form raw string with a single literal placeholder", func(ctx SpecContext) { + resolver := symbol.MapResolver(map[string]symbol.Symbol{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + }) + output := MustSucceed(compileWithHostImports( + ctx, + `trig -> f"v={42}" -> log`, + resolver, + )) + mod := MustSucceed(r.Instantiate(ctx, output.WASM)) + strMod.SetMemory(mod.Memory()) + synth := mod.ExportedFunction("fmt$fmt_0") + Expect(synth).ToNot(BeNil()) + results := MustSucceed(synth.Call(ctx)) + Expect(results).To(HaveLen(1)) + handle := uint32(results[0]) + Expect(handle).To(BeNumerically(">", 0)) + str, ok := strState.Get(handle) + Expect(ok).To(BeTrue()) + Expect(str).To(Equal("v=42")) + }) + + It("Compiles a placeholder with a numeric format spec", func(ctx SpecContext) { + resolver := symbol.MapResolver(map[string]symbol.Symbol{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + }) + output := MustSucceed(compileWithHostImports( + ctx, + `trig -> f"v={f64(3.14159):.2f}" -> log`, + resolver, + )) + mod := MustSucceed(r.Instantiate(ctx, output.WASM)) + strMod.SetMemory(mod.Memory()) + synth := mod.ExportedFunction("fmt$fmt_0") + Expect(synth).ToNot(BeNil()) + results := MustSucceed(synth.Call(ctx)) + handle := uint32(results[0]) + str, ok := strState.Get(handle) + Expect(ok).To(BeTrue()) + Expect(str).To(Equal("v=3.14")) + }) + + It("Compiles multiple placeholders separated by literals", func(ctx SpecContext) { + resolver := symbol.MapResolver(map[string]symbol.Symbol{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + }) + output := MustSucceed(compileWithHostImports( + ctx, + `trig -> f"a={1} b={i32(2):05d} c={f64(3.14):.2f}" -> log`, + resolver, + )) + mod := MustSucceed(r.Instantiate(ctx, output.WASM)) + strMod.SetMemory(mod.Memory()) + synth := mod.ExportedFunction("fmt$fmt_0") + Expect(synth).ToNot(BeNil()) + results := MustSucceed(synth.Call(ctx)) + handle := uint32(results[0]) + str, ok := strState.Get(handle) + Expect(ok).To(BeTrue()) + Expect(str).To(Equal("a=1 b=00002 c=3.14")) + }) + + It("Compiles a placeholder-only body with no surrounding literals", func(ctx SpecContext) { + resolver := symbol.MapResolver(map[string]symbol.Symbol{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + }) + output := MustSucceed(compileWithHostImports( + ctx, + `trig -> f"{42}" -> log`, + resolver, + )) + mod := MustSucceed(r.Instantiate(ctx, output.WASM)) + strMod.SetMemory(mod.Memory()) + synth := mod.ExportedFunction("fmt$fmt_0") + Expect(synth).ToNot(BeNil()) + results := MustSucceed(synth.Call(ctx)) + handle := uint32(results[0]) + str, ok := strState.Get(handle) + Expect(ok).To(BeTrue()) + Expect(str).To(Equal("42")) + }) + + It("Numbers multiple synthetic functions independently", func(ctx SpecContext) { + resolver := symbol.MapResolver(map[string]symbol.Symbol{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 100}, + "log_a": {Name: "log_a", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + "log_b": {Name: "log_b", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 102}, + }) + output := MustSucceed(compileWithHostImports(ctx, ` + trig -> `+`f"first={1}"`+` -> log_a + trig -> `+`f"second={2}"`+` -> log_b + `, resolver)) + mod := MustSucceed(r.Instantiate(ctx, output.WASM)) + strMod.SetMemory(mod.Memory()) + Expect(mod.ExportedFunction("fmt$fmt_0")).ToNot(BeNil()) + Expect(mod.ExportedFunction("fmt$fmt_1")).ToNot(BeNil()) + }) + }) + Describe("Numeric Type Execution", func() { DescribeTable("numeric type literals", func(ctx SpecContext, body string, expected any) { diff --git a/arc/go/compiler/expression/cast.go b/arc/go/compiler/expression/cast.go index 151e7de4b9..98aeb50db4 100644 --- a/arc/go/compiler/expression/cast.go +++ b/arc/go/compiler/expression/cast.go @@ -26,14 +26,7 @@ func compileTypeCast( if !targetType.IsValid() { return types.Type{}, errors.New("unknown cast target type") } - // Pass the target type as a hint so literals can be emitted with the correct type - // directly. For str targets, suppress the hint so a numeric literal gets its natural - // type rather than falling through parseIntegerLiteral's default branch. - hint := targetType - if targetType.Kind == types.KindString { - hint = types.Type{} - } - sourceType, err := Compile(context.Child(ctx, ctx.AST.Expression()).WithHint(hint)) + sourceType, err := Compile(context.Child(ctx, ctx.AST.Expression()).WithHint(targetType)) if err != nil { return types.Type{}, err } diff --git a/arc/go/compiler/expression/cast_test.go b/arc/go/compiler/expression/cast_test.go index 560c9ec98a..11f8e01e6b 100644 --- a/arc/go/compiler/expression/cast_test.go +++ b/arc/go/compiler/expression/cast_test.go @@ -313,6 +313,22 @@ var _ = Describe("Type Cast Compilation", func() { OpF64Neg, OpCall, uint32(0), ), + Entry( + "float literal to str (negative zero)", + "str(-0.0)", + types.String(), + OpF64Const, float64(0), + OpF64Neg, + OpCall, uint32(0), + ), + Entry( + "float literal to str (negative zero with trailing zeros)", + "str(-0.0000)", + types.String(), + OpF64Const, float64(0), + OpF64Neg, + OpCall, uint32(0), + ), Entry( "float literal to str (trailing zero)", "str(1.0)", diff --git a/arc/go/compiler/expression/fmt_str.go b/arc/go/compiler/expression/fmt_str.go new file mode 100644 index 0000000000..2e3738eff6 --- /dev/null +++ b/arc/go/compiler/expression/fmt_str.go @@ -0,0 +1,97 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package expression + +import ( + "github.com/antlr4-go/antlr/v4" + "github.com/synnaxlabs/arc/compiler/context" + "github.com/synnaxlabs/arc/literal" + "github.com/synnaxlabs/arc/parser" + "github.com/synnaxlabs/arc/types" + "github.com/synnaxlabs/x/errors" +) + +// EmitFmtSegments lowers parsed format segments into WASM on ctx.Writer: +// literals emit string.from_literal, placeholders compile their expression. +// Assumes the analyzer has already run literal.FmtStrValidateSpec on every +// placeholder spec; the compiler emits spec bytes verbatim without revalidation. +func EmitFmtSegments[T antlr.ParserRuleContext]( + ctx context.Context[T], + segments []literal.FmtStrSegment, +) (types.Type, error) { + if len(segments) == 0 { + emitLiteralSegment(ctx, "") + return types.String(), nil + } + if err := emitFmtSegment(ctx, segments[0]); err != nil { + return types.Type{}, err + } + for _, seg := range segments[1:] { + if err := emitFmtSegment(ctx, seg); err != nil { + return types.Type{}, err + } + ctx.Resolver.EmitStringConcat(ctx.Writer, ctx.WriterID) + } + return types.String(), nil +} + +func emitFmtSegment[T antlr.ParserRuleContext]( + ctx context.Context[T], + seg literal.FmtStrSegment, +) error { + if !seg.IsPlaceholder { + emitLiteralSegment(ctx, seg.Text) + return nil + } + expr, diags := parser.ParseExpression(seg.Text) + if diags != nil && !diags.Ok() { + return errors.Newf("invalid placeholder %q: %s", seg.Text, diags.String()) + } + t, err := Compile(context.Child(ctx, expr).WithHint(types.Type{})) + if err != nil { + return err + } + if t.Kind == types.KindString { + if seg.Spec != "" { + emitSpecBytes(ctx, seg.Spec) + return ctx.Resolver.EmitStringFormat(ctx.Writer, ctx.WriterID) + } + return nil + } + if t.IsNumeric() { + if seg.Spec != "" { + emitSpecBytes(ctx, seg.Spec) + return ctx.Resolver.EmitNumericFormat(ctx.Writer, ctx.WriterID, t) + } + return ctx.Resolver.EmitNumericToString(ctx.Writer, ctx.WriterID, t) + } + return errors.Newf( + "placeholder %q has type %s; only numeric and string types are supported", + seg.Text, t, + ) +} + +func emitSpecBytes[T antlr.ParserRuleContext](ctx context.Context[T], spec string) { + specBytes := []byte(spec) + offset := ctx.Module.AddData(specBytes) + ctx.Writer.WriteI32Const(int32(offset)) + ctx.Writer.WriteI32Const(int32(len(specBytes))) +} + +func emitLiteralSegment[T antlr.ParserRuleContext]( + ctx context.Context[T], + text string, +) { + bytes := []byte(text) + offset := ctx.Module.AddData(bytes) + ctx.Writer.WriteI32Const(int32(offset)) + ctx.Writer.WriteI32Const(int32(len(bytes))) + ctx.Resolver.EmitStringFromLiteral(ctx.Writer, ctx.WriterID) +} diff --git a/arc/go/compiler/expression/fmt_str_test.go b/arc/go/compiler/expression/fmt_str_test.go new file mode 100644 index 0000000000..00e9f15cbf --- /dev/null +++ b/arc/go/compiler/expression/fmt_str_test.go @@ -0,0 +1,189 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package expression_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ccontext "github.com/synnaxlabs/arc/compiler/context" + "github.com/synnaxlabs/arc/compiler/expression" + . "github.com/synnaxlabs/arc/compiler/testutil" + . "github.com/synnaxlabs/arc/compiler/wasm" + "github.com/synnaxlabs/arc/parser" + "github.com/synnaxlabs/arc/symbol" + "github.com/synnaxlabs/arc/types" + . "github.com/synnaxlabs/x/testutil" +) + +var _ = Describe("Format String Compilation", func() { + Describe("Single Placeholder, No Spec", func() { + DescribeTable("compiles numeric literal placeholder via from_ conversion", + expectExpression, + + Entry( + "integer literal placeholder", + `f"{42}"`, + types.String(), + OpI64Const, int64(42), + OpCall, uint32(0), + ), + Entry( + "float literal placeholder", + `f"{3.14}"`, + types.String(), + OpF64Const, float64(3.14), + OpCall, uint32(0), + ), + Entry( + "explicit i32 cast placeholder", + `f"{i32(7)}"`, + types.String(), + OpI32Const, int32(7), + OpCall, uint32(0), + ), + Entry( + "explicit u8 cast placeholder", + `f"{u8(255)}"`, + types.String(), + OpI32Const, int32(255), + OpCall, uint32(0), + ), + Entry( + "explicit f32 cast placeholder", + `f"{f32(2.5)}"`, + types.String(), + OpF32Const, float32(2.5), + OpCall, uint32(0), + ), + ) + }) + + Describe("Single Placeholder, With Format Spec", func() { + DescribeTable("compiles numeric placeholder with spec via emitSpecBytes + format_", + expectExpression, + + Entry( + "i32 with :05d", + `f"{i32(7):05d}"`, + types.String(), + OpI32Const, int32(7), + OpI32Const, int32(0), + OpI32Const, int32(3), + OpCall, uint32(0), + ), + Entry( + "f64 with :.2f", + `f"{f64(3.14):.2f}"`, + types.String(), + OpF64Const, float64(3.14), + OpI32Const, int32(0), + OpI32Const, int32(3), + OpCall, uint32(0), + ), + Entry( + "u8 with :x", + `f"{u8(255):x}"`, + types.String(), + OpI32Const, int32(255), + OpI32Const, int32(0), + OpI32Const, int32(1), + OpCall, uint32(0), + ), + ) + }) + + Describe("String Variable Placeholder", func() { + It("compiles string variable placeholder with no spec as identity", func(bCtx SpecContext) { + bytecode, exprType := compileWithAnalyzer(bCtx, `f"{name}"`, symbol.MapResolver{ + "name": scalarSymbol("name", types.String(), 0), + }) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles string variable placeholder with spec via format_string", func(bCtx SpecContext) { + bytecode, exprType := compileWithAnalyzer(bCtx, `f"{name:s}"`, symbol.MapResolver{ + "name": scalarSymbol("name", types.String(), 0), + }) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + }) + + Describe("Mixed Literal and Placeholder Segments", func() { + It("compiles literal + placeholder with concat", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, `f"x={42}"`) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles placeholder + literal with concat", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, `f"{42} done"`) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles two placeholders separated by literal with two concat ops", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, `f"{1} and {2}"`) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles three placeholders with mixed specs", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, `f"{1}, {i32(2):05d}, {f64(3.14):.2f}"`) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles adjacent placeholders with no separator", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, `f"{1}{2}"`) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles a multi-line format string with literal newlines around a placeholder", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, "f`line1\n{42}\nline3`") + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles a multi-line format string with multiple placeholders across lines", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, "f`a={1}\nb={2}`") + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles an rf-prefixed format string with placeholder and backslash literal", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, `rf"path\to\{42}"`) + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + + It("compiles an rf-prefixed multi-line format string with placeholder across newlines", func(bCtx SpecContext) { + bytecode, exprType := compileExpression(bCtx, "rf`path\\to\n{42}`") + Expect(exprType).To(Equal(types.String())) + Expect(bytecode).ToNot(BeEmpty()) + }) + }) + + Describe("Malformed Format String Body", func() { + It("propagates literal.FmtStrParse errors from compileRawStringLiteral", func(bCtx SpecContext) { + // `{` parses as a raw-string token, but the body is malformed: + // literal.FmtStrParse rejects an unmatched '{', exercising the second + // error branch in compileRawStringLiteral. + expr := MustSucceed(parser.ParseExpression(`f"{"`)) + ctx := NewContext(bCtx) + _, err := expression.Compile(ccontext.Child(ctx, expr)) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unmatched")) + }) + }) + +}) diff --git a/arc/go/compiler/expression/literal.go b/arc/go/compiler/expression/literal.go index 1d79aa63bb..402ce2cc65 100644 --- a/arc/go/compiler/expression/literal.go +++ b/arc/go/compiler/expression/literal.go @@ -23,11 +23,8 @@ func compileLiteral( if num := ctx.AST.NumericLiteral(); num != nil { return compileNumericLiteral(context.Child(ctx, num)) } - if str := ctx.AST.STR_LITERAL(); str != nil { - return compileStringLiteral(ctx, literal.ParseString, str.GetText()) - } - if str := ctx.AST.STR_LITERAL_RAW(); str != nil { - return compileStringLiteral(ctx, literal.ParseRawString, str.GetText()) + if strTerm := parser.StringTerminal(ctx.AST); strTerm != nil { + return compileStringLiteral(ctx, strTerm.GetText()) } if series := ctx.AST.SeriesLiteral(); series != nil { return compileSeriesLiteral(context.Child(ctx, series)) @@ -37,18 +34,28 @@ func compileLiteral( func compileStringLiteral( ctx context.Context[parser.ILiteralContext], - parse func(string, types.Type) (literal.ParsedValue, error), text string, ) (types.Type, error) { - parsed, err := parse(text, types.String()) - if err != nil { - return types.Type{}, err + body, flags, ok := literal.StripQuotes(text) + if !ok { + return types.Type{}, errors.Newf("invalid string literal: %s", text) + } + value := body + if !flags.Raw { + var err error + value, err = literal.UnescapeString(body, flags.Multi) + if err != nil { + return types.Type{}, errors.Wrapf(err, "invalid string literal %s", text) + } + } + if flags.Format { + segments, err := literal.FmtStrParse(value) + if err != nil { + return types.Type{}, err + } + return EmitFmtSegments(ctx, segments) } - strBytes := []byte(parsed.Value.(string)) - offset := ctx.Module.AddData(strBytes) - ctx.Writer.WriteI32Const(int32(offset)) - ctx.Writer.WriteI32Const(int32(len(strBytes))) - ctx.Resolver.EmitStringFromLiteral(ctx.Writer, ctx.WriterID) + emitLiteralSegment(ctx, value) return types.String(), nil } @@ -100,7 +107,8 @@ func compileNumericLiteral( ) (types.Type, error) { targetType := ctx.Hint - if !targetType.IsValid() { + if !targetType.IsValid() || !targetType.IsNumeric() { + targetType = types.Type{} if parent := ctx.AST.GetParent(); parent != nil { if litCtx, ok := parent.(parser.ILiteralContext); ok { if inferredType, ok := ctx.TypeMap[litCtx]; ok { diff --git a/arc/go/compiler/expression/literal_test.go b/arc/go/compiler/expression/literal_test.go index d249a9ee31..dbc2d2ced3 100644 --- a/arc/go/compiler/expression/literal_test.go +++ b/arc/go/compiler/expression/literal_test.go @@ -130,47 +130,111 @@ var _ = Describe("Literal Compilation", func() { ), ) - DescribeTable("should compile raw string literals", + DescribeTable("should compile string literals (no placeholders)", expectExpression, Entry( - "simple raw", - "`hi`", + "format string, simple body", + `f"hi"`, types.String(), OpI32Const, int32(0), OpI32Const, int32(2), OpCall, uint32(0), ), Entry( - "empty raw", - "``", + "format string, empty body", + `f""`, types.String(), OpI32Const, int32(0), OpI32Const, int32(0), OpCall, uint32(0), ), Entry( - "multi-line raw preserves newline", - "`a\nb`", + "format string, newline escape processed", + `f"a\nb"`, types.String(), OpI32Const, int32(0), OpI32Const, int32(3), OpCall, uint32(0), ), Entry( - "escape chars verbatim", - "`\\n`", + "format string, escaped backslash", + `f"\\n"`, types.String(), OpI32Const, int32(0), OpI32Const, int32(2), OpCall, uint32(0), ), Entry( - "escaped backtick", - "`say \\`hi\\``", + "plain string", + `"hello"`, + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(5), + OpCall, uint32(0), + ), + Entry( + "multi-line string preserves real newline", + "`a\nb`", + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(3), + OpCall, uint32(0), + ), + Entry( + "multi-line string with three lines", + "`a\nb\nc`", + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(5), + OpCall, uint32(0), + ), + Entry( + "multi-line format string, no placeholders", + "f`hello\nworld`", + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(11), + OpCall, uint32(0), + ), + Entry( + "raw string preserves backslash-n verbatim", + `r"a\nb"`, + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(4), + OpCall, uint32(0), + ), + Entry( + "raw string preserves backslash sequence", + `r"C:\path"`, + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(7), + OpCall, uint32(0), + ), + Entry( + "raw multi-line preserves backslash and real newline", + "r`a\\nb\nc`", + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(6), + OpCall, uint32(0), + ), + Entry( + "rf prefix with no placeholders preserves backslashes", + `rf"a\nb"`, + types.String(), + OpI32Const, int32(0), + OpI32Const, int32(4), + OpCall, uint32(0), + ), + Entry( + "rf multi-line with no placeholders preserves backslash and real newline", + "rf`a\\nb\nc`", types.String(), OpI32Const, int32(0), - OpI32Const, int32(8), + OpI32Const, int32(6), OpCall, uint32(0), ), ) diff --git a/arc/go/compiler/resolve/emit.go b/arc/go/compiler/resolve/emit.go index 1bbbc8098a..2675e53d00 100644 --- a/arc/go/compiler/resolve/emit.go +++ b/arc/go/compiler/resolve/emit.go @@ -325,24 +325,44 @@ func (r *Resolver) EmitStringLen(w *wasm.Writer, wID int) { // EmitNumericToString emits a call to the string.from_* host fn matching // the source numeric type. Shared by the str() typecast and f-strings. func (r *Resolver) EmitNumericToString(w *wasm.Writer, wID int, from types.Type) error { - var suffix string - switch from.Kind { + suffix, err := numericSuffix(from) + if err != nil { + return err + } + return r.EmitFixedCall(w, wID, "string.from_"+suffix) +} + +func (r *Resolver) EmitNumericFormat(w *wasm.Writer, wID int, from types.Type) error { + suffix, err := numericSuffix(from) + if err != nil { + return err + } + return r.EmitFixedCall(w, wID, "string.format_"+suffix) +} + +// EmitStringFormat emits a call to string.format_string. +func (r *Resolver) EmitStringFormat(w *wasm.Writer, wID int) error { + return r.EmitFixedCall(w, wID, "string.format_string") +} + +func numericSuffix(t types.Type) (string, error) { + switch t.Kind { case types.KindI8, types.KindI16, types.KindI32: - suffix = "i32" + return "i32", nil case types.KindU8, types.KindU16, types.KindU32: - suffix = "u32" - case types.KindI64: - suffix = "i64" + return "u32", nil + case types.KindI64, types.KindIntegerConstant: + return "i64", nil case types.KindU64: - suffix = "u64" + return "u64", nil case types.KindF32: - suffix = "f32" - case types.KindF64: - suffix = "f64" - default: - return errors.Newf("cannot convert %s to str", from) + return "f32", nil + case types.KindF64, + types.KindFloatConstant, types.KindNumericConstant, + types.KindExactIntegerFloatConstant: + return "f64", nil } - return r.EmitFixedCall(w, wID, "string.from_"+suffix) + return "", errors.Newf("cannot convert %s to str", t) } // EmitMathPow emits a call to math.pow for the given type. diff --git a/arc/go/compiler/resolve/emit_test.go b/arc/go/compiler/resolve/emit_test.go index c9a05f3364..15966159cf 100644 --- a/arc/go/compiler/resolve/emit_test.go +++ b/arc/go/compiler/resolve/emit_test.go @@ -59,6 +59,64 @@ var _ = Describe("EmitNumericToString", func() { }) }) +var _ = Describe("EmitNumericFormat", func() { + DescribeTable("Should dispatch to the format_ host fn matching the source type", + func(from types.Type, wantWASMName string) { + r := resolve.NewResolver(stringSymbolResolver) + w := wasm.NewWriter() + wID := r.TrackWriter(w) + + Expect(r.EmitNumericFormat(w, wID, from)).To(Succeed()) + + m := wasm.NewModule() + r.Finalize(m) + Expect(m.ImportNames()).To(ConsistOf(wantWASMName)) + }, + Entry("i8 -> format_i32", types.I8(), "format_i32"), + Entry("i16 -> format_i32", types.I16(), "format_i32"), + Entry("i32 -> format_i32", types.I32(), "format_i32"), + Entry("u8 -> format_u32", types.U8(), "format_u32"), + Entry("u16 -> format_u32", types.U16(), "format_u32"), + Entry("u32 -> format_u32", types.U32(), "format_u32"), + Entry("i64 -> format_i64", types.I64(), "format_i64"), + Entry("u64 -> format_u64", types.U64(), "format_u64"), + Entry("f32 -> format_f32", types.F32(), "format_f32"), + Entry("f64 -> format_f64", types.F64(), "format_f64"), + ) + + It("Should return an error for non-numeric source types", func() { + r := resolve.NewResolver(stringSymbolResolver) + w := wasm.NewWriter() + wID := r.TrackWriter(w) + + Expect(r.EmitNumericFormat(w, wID, types.String())). + To(MatchError(ContainSubstring("cannot convert"))) + }) +}) + +var _ = Describe("EmitStringFormat", func() { + It("Should emit an import for string.format_string", func() { + r := resolve.NewResolver(stringSymbolResolver) + w := wasm.NewWriter() + wID := r.TrackWriter(w) + + Expect(r.EmitStringFormat(w, wID)).To(Succeed()) + + m := wasm.NewModule() + r.Finalize(m) + Expect(m.ImportNames()).To(ConsistOf("format_string")) + }) + + It("Should return an error when no SymbolResolver is configured", func() { + r := resolve.NewResolver(nil) + w := wasm.NewWriter() + wID := r.TrackWriter(w) + + Expect(r.EmitStringFormat(w, wID)). + To(MatchError(ContainSubstring("no symbol resolver"))) + }) +}) + var _ = Describe("EmitFixedCall", func() { It("Should resolve the signature from the SymbolResolver and emit an import", func() { r := resolve.NewResolver(stringSymbolResolver) diff --git a/arc/go/fmt_str_test.go b/arc/go/fmt_str_test.go new file mode 100644 index 0000000000..14fe7570e0 --- /dev/null +++ b/arc/go/fmt_str_test.go @@ -0,0 +1,535 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package arc_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/synnaxlabs/arc/stl/channel" + "github.com/synnaxlabs/arc/types" + "github.com/synnaxlabs/x/telem" +) + +var _ = Describe("format-string end-to-end runtime", func() { + lastString := func(fr telem.Frame[uint32], key uint32) string { + ch := fr.Get(key) + Expect(ch.Series).ToNot(BeEmpty(), "channel %d not written", key) + s := ch.Series[len(ch.Series)-1] + vals := telem.UnmarshalSeries[string](s) + Expect(vals).ToNot(BeEmpty()) + return vals[len(vals)-1] + } + + runFmtTrigger := func(ctx SpecContext, source string) string { + resolver := channelSymbols(map[string]channelDef{ + "trig": {types.U8(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `func f() { + log = `+source+` + } + trig -> f{}`, resolver, + channel.Digest{Key: 100, DataType: telem.Uint8T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[uint8](1)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + return lastString(out, 101) + } + + runFmtChannel := func( + ctx SpecContext, + source, arcType string, + valueType types.Type, + valueDT telem.DataType, + ingest func(*runtimeHarness), + ) string { + resolver := channelSymbols(map[string]channelDef{ + "v": {valueType, 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `func f(val `+arcType+`) { + log = `+source+` + } + v -> f{}`, resolver, + channel.Digest{Key: 100, DataType: valueDT}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + ingest(h) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + return lastString(out, 101) + } + + Describe("Literal raw strings (no placeholders)", func() { + DescribeTable("emits literal text verbatim", + func(ctx SpecContext, source, expected string) { + Expect(runFmtTrigger(ctx, source)).To(Equal(expected)) + }, + Entry("plain word", `f"static"`, "static"), + Entry("single space", `f" "`, " "), + Entry("multi-word with punctuation", `f"hello, world!"`, "hello, world!"), + Entry("doubled open brace", `f"{{"`, "{"), + Entry("doubled close brace", `f"}}"`, "}"), + Entry("bare close brace is literal", `f"}"`, "}"), + Entry("doubled braces around literal", `f"{{x}}"`, "{x}"), + Entry("doubled braces mixed with text", `f"pre {{ mid }} post"`, "pre { mid } post"), + Entry("embedded double quotes", `f"he said \"hi\""`, `he said "hi"`), + ) + }) + + Describe("Single placeholder, no format spec", func() { + DescribeTable("renders numeric literal placeholders via str() conversion", + func(ctx SpecContext, source, expected string) { + Expect(runFmtTrigger(ctx, source)).To(Equal(expected)) + }, + Entry("integer literal", `f"the answer is {42}"`, "the answer is 42"), + Entry("negative integer literal", `f"negative: {-7}"`, "negative: -7"), + Entry("zero", `f"zero: {0}"`, "zero: 0"), + Entry("float literal", `f"pi: {3.14}"`, "pi: 3.14"), + Entry("float literal trailing zeros stripped", `f"x: {1.0}"`, "x: 1"), + Entry("explicit f32 cast", `f"x: {f32(3.14)}"`, "x: 3.14"), + Entry("explicit f64 cast", `f"x: {f64(3.14)}"`, "x: 3.14"), + Entry("explicit i32 cast", `f"x: {i32(42)}"`, "x: 42"), + Entry("explicit u32 cast", `f"x: {u32(42)}"`, "x: 42"), + Entry("explicit u8 cast", `f"x: {u8(255)}"`, "x: 255"), + ) + + DescribeTable("renders channel value of each numeric type", + func( + ctx SpecContext, + arcType string, + valueType types.Type, + valueDT telem.DataType, + ingest func(*runtimeHarness), + expected string, + ) { + Expect(runFmtChannel(ctx, `f"x={val}"`, arcType, valueType, valueDT, ingest)). + To(Equal(expected)) + }, + Entry("u8 channel", "u8", types.U8(), telem.Uint8T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint8](7)) }, "x=7"), + Entry("u16 channel", "u16", types.U16(), telem.Uint16T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint16](7)) }, "x=7"), + Entry("u32 channel", "u32", types.U32(), telem.Uint32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint32](4000000000)) }, "x=4000000000"), + Entry("u64 channel", "u64", types.U64(), telem.Uint64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint64](18000000000000000000)) }, "x=18000000000000000000"), + Entry("i32 channel", "i32", types.I32(), telem.Int32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[int32](-42)) }, "x=-42"), + Entry("i64 channel", "i64", types.I64(), telem.Int64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[int64](-1700000000)) }, "x=-1700000000"), + Entry("f32 channel", "f32", types.F32(), telem.Float32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[float32](3.14)) }, "x=3.14"), + Entry("f64 channel high precision", "f64", types.F64(), telem.Float64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[float64](0.1234567890123456)) }, "x=0.1234567890123456"), + ) + + It("renders a string-typed placeholder bound to a local string", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "trig": {types.U8(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, ` + func f() { + name := "probe" + log = `+`f"hello, {name}"`+` + } + trig -> f{}`, resolver, + channel.Digest{Key: 100, DataType: telem.Uint8T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[uint8](1)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("hello, probe")) + }) + }) + + Describe("Single placeholder with valid format spec", func() { + DescribeTable("integer types format with valid Go fmt verbs", + func(ctx SpecContext, source, expected string) { + Expect(runFmtTrigger(ctx, source)).To(Equal(expected)) + }, + Entry("decimal", `f"{i32(42):d}"`, "42"), + Entry("decimal with width", `f"{i32(42):5d}"`, " 42"), + Entry("decimal zero-padded", `f"{i32(7):05d}"`, "00007"), + Entry("decimal with sign", `f"{i32(42):+d}"`, "+42"), + Entry("hex lower", `f"{i32(255):x}"`, "ff"), + Entry("hex upper", `f"{i32(255):X}"`, "FF"), + Entry("hex zero-padded", `f"{i32(255):04x}"`, "00ff"), + Entry("octal", `f"{i32(8):o}"`, "10"), + Entry("binary", `f"{i32(5):b}"`, "101"), + Entry("negative decimal", `f"{i32(-42):d}"`, "-42"), + ) + + DescribeTable("integer channel values format with valid specs (i8/i16/i32/i64 promotion)", + func( + ctx SpecContext, + source, arcType string, + valueType types.Type, + valueDT telem.DataType, + ingest func(*runtimeHarness), + expected string, + ) { + Expect(runFmtChannel(ctx, source, arcType, valueType, valueDT, ingest)). + To(Equal(expected)) + }, + Entry("i32 channel :05d", `f"{val:05d}"`, "i32", types.I32(), telem.Int32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[int32](7)) }, "00007"), + Entry("i32 channel :+d", `f"{val:+d}"`, "i32", types.I32(), telem.Int32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[int32](42)) }, "+42"), + Entry("i64 channel :d", `f"{val:d}"`, "i64", types.I64(), telem.Int64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[int64](1700000000)) }, "1700000000"), + Entry("i64 channel :x", `f"{val:x}"`, "i64", types.I64(), telem.Int64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[int64](255)) }, "ff"), + ) + + DescribeTable("unsigned integer types format with valid Go fmt verbs", + func(ctx SpecContext, source, expected string) { + Expect(runFmtTrigger(ctx, source)).To(Equal(expected)) + }, + Entry("u8 decimal", `f"{u8(255):d}"`, "255"), + Entry("u8 hex", `f"{u8(255):x}"`, "ff"), + Entry("u8 binary", `f"{u8(255):b}"`, "11111111"), + Entry("u32 decimal", `f"{u32(4000000000):d}"`, "4000000000"), + Entry("u32 hex zero-padded", `f"{u32(255):08x}"`, "000000ff"), + Entry("u32 octal", `f"{u32(8):o}"`, "10"), + Entry("u64 decimal", `f"{u64(12345):d}"`, "12345"), + Entry("u64 hex", `f"{u64(255):x}"`, "ff"), + ) + + DescribeTable("unsigned integer channel values format with valid specs (u8/u16/u32/u64 promotion)", + func( + ctx SpecContext, + source, arcType string, + valueType types.Type, + valueDT telem.DataType, + ingest func(*runtimeHarness), + expected string, + ) { + Expect(runFmtChannel(ctx, source, arcType, valueType, valueDT, ingest)). + To(Equal(expected)) + }, + Entry("u8 channel :d", `f"{val:d}"`, "u8", types.U8(), telem.Uint8T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint8](255)) }, "255"), + Entry("u8 channel :x (promoted to u32)", `f"{val:x}"`, "u8", types.U8(), telem.Uint8T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint8](255)) }, "ff"), + Entry("u16 channel :d", `f"{val:d}"`, "u16", types.U16(), telem.Uint16T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint16](65000)) }, "65000"), + Entry("u32 channel :X", `f"{val:X}"`, "u32", types.U32(), telem.Uint32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint32](255)) }, "FF"), + Entry("u64 channel :x", `f"{val:x}"`, "u64", types.U64(), telem.Uint64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[uint64](255)) }, "ff"), + ) + + DescribeTable("float types format with valid Go fmt verbs", + func(ctx SpecContext, source, expected string) { + Expect(runFmtTrigger(ctx, source)).To(Equal(expected)) + }, + Entry("f64 fixed default", `f"{f64(3.14159):f}"`, "3.141590"), + Entry("f64 fixed 2 decimals", `f"{f64(3.14159):.2f}"`, "3.14"), + Entry("f64 fixed 4 decimals", `f"{f64(3.14159):.4f}"`, "3.1416"), + Entry("f64 fixed width.precision", `f"{f64(3.14):8.3f}"`, " 3.140"), + Entry("f64 fixed 0 decimals", `f"{f64(3.7):.0f}"`, "4"), + Entry("f64 scientific lower", `f"{f64(12345.678):e}"`, "1.234568e+04"), + Entry("f64 scientific upper", `f"{f64(12345.678):E}"`, "1.234568E+04"), + Entry("f64 general lower", `f"{f64(0.000123):g}"`, "0.000123"), + Entry("f64 general upper", `f"{f64(0.000123):G}"`, "0.000123"), + Entry("f32 fixed 2 decimals", `f"{f32(3.14159):.2f}"`, "3.14"), + ) + + DescribeTable("float channel values format with valid specs", + func( + ctx SpecContext, + source, arcType string, + valueType types.Type, + valueDT telem.DataType, + ingest func(*runtimeHarness), + expected string, + ) { + Expect(runFmtChannel(ctx, source, arcType, valueType, valueDT, ingest)). + To(Equal(expected)) + }, + Entry("f32 channel :.2f", `f"{val:.2f}"`, "f32", types.F32(), telem.Float32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[float32](3.14159)) }, "3.14"), + Entry("f32 channel :e", `f"{val:e}"`, "f32", types.F32(), telem.Float32T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[float32](12345.678)) }, "1.234568e+04"), + Entry("f64 channel :.4f", `f"{val:.4f}"`, "f64", types.F64(), telem.Float64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[float64](3.14159)) }, "3.1416"), + Entry("f64 channel :g", `f"{val:g}"`, "f64", types.F64(), telem.Float64T, + func(h *runtimeHarness) { h.Ingest(100, telem.NewSeriesV[float64](0.000123)) }, "0.000123"), + ) + }) + + Describe("Documented Examples table (syntax.mdx)", func() { + runFmtExample := func(ctx SpecContext, declarations, body string) string { + resolver := channelSymbols(map[string]channelDef{ + "trig": {types.U8(), 100}, + "log": {types.String(), 101}, + }) + src := `func f() { + ` + declarations + ` + log = ` + body + ` + } + trig -> f{}` + h := newRuntimeHarness(ctx, src, resolver, + channel.Digest{Key: 100, DataType: telem.Uint8T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[uint8](1)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + return lastString(out, 101) + } + DescribeTable("each documented spec produces the documented output", + func(ctx SpecContext, declarations, body, expected string) { + Expect(runFmtExample(ctx, declarations, body)).To(Equal(expected)) + }, + Entry("#b alternate-form binary", "", `f"{i32(5):#b}"`, "0b101"), + Entry("#o alternate-form octal", "", `f"{i32(8):#o}"`, "010"), + Entry("#x alternate-form hex", "", `f"{i32(255):#x}"`, "0xff"), + Entry("X uppercase hex", "", `f"{i32(255):X}"`, "FF"), + Entry("E uppercase scientific", "", `f"{f64(3.14):E}"`, "3.140000E+00"), + Entry("G uppercase compact", "", `f"{f64(3.14):G}"`, "3.14"), + Entry("+d signed decimal", "", `f"{i32(42):+d}"`, "+42"), + Entry("space d leading space", "", `f"{i32(42): d}"`, " 42"), + Entry("5d width", "", `f"{i32(42):5d}"`, " 42"), + Entry("-5d left-aligned width", "", `f"{i32(42):-5d}"`, "42 "), + Entry("05d zero-padded width", "", `f"{i32(42):05d}"`, "00042"), + Entry("5s string width", `name := "ok"`, `f"{name:5s}"`, " ok"), + Entry("-5s left-aligned string width", `name := "ok"`, `f"{name:-5s}"`, "ok "), + Entry(".2f float precision", "", `f"{f64(3.14159):.2f}"`, "3.14"), + Entry("+f signed float", "", `f"{f64(3.14):+f}"`, "+3.140000"), + Entry("6.2f width and precision", "", `f"{f64(3.14):6.2f}"`, " 3.14"), + Entry("+08.2f sign zero-pad width precision", "", `f"{f64(3.14):+08.2f}"`, "+0003.14"), + Entry("#06x alternate-form zero-pad hex", "", `f"{i32(255):#06x}"`, "0x0000ff"), + ) + }) + + Describe("Multiple placeholders and interleaved escapes", func() { + DescribeTable("multi-segment concat chains", + func(ctx SpecContext, source, expected string) { + Expect(runFmtTrigger(ctx, source)).To(Equal(expected)) + }, + Entry("two placeholders with literal between", + `f"pre {1} mid {2} post"`, "pre 1 mid 2 post"), + Entry("adjacent placeholders no separator", + `f"{1}{2}"`, "12"), + Entry("placeholder at start", + `f"{42} trailing"`, "42 trailing"), + Entry("placeholder at end", + `f"leading {42}"`, "leading 42"), + Entry("doubled braces interleaved with placeholders", + `f"{{ {7} }}"`, "{ 7 }"), + Entry("three placeholders with mixed specs", + `f"{1}, {i32(2):05d}, {f64(3.14):.2f}"`, "1, 00002, 3.14"), + Entry("doubled braces around placeholder", + `f"{{{42}}}"`, "{42}"), + Entry("raw string with backslash adjacent to placeholder", + `rf"C:\logs\{42}.txt"`, `C:\logs\42.txt`), + Entry("raw string with backslash adjacent and doubled-brace literal", + `rf"C:\out\{{tag}}-{42}.bin"`, `C:\out\{tag}-42.bin`), + Entry("raw format string with only doubled braces (no placeholder)", + `rf"C:\logs\{{abc}}.txt"`, `C:\logs\{abc}.txt`), + ) + }) + + Describe("Flow-form synthetic functions", func() { + It("synthesizes a fmt$ function for a raw string with placeholders in flow form", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "sensor": {types.F32(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `sensor -> f"v={sensor}" -> log`, resolver, + channel.Digest{Key: 100, DataType: telem.Float32T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[float32](3.14)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("v=3.14")) + }) + + It("preserves a numeric format spec on a flow-form synthetic", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "sensor": {types.F64(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `sensor -> f"v={sensor:.2f}" -> log`, resolver, + channel.Digest{Key: 100, DataType: telem.Float64T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[float64](3.14159)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("v=3.14")) + }) + + It("synthesizes a fmt$ function for a multi-channel placeholder body in flow form", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "sensor": {types.F32(), 100}, + "t": {types.I32(), 102}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `sensor -> f"v={sensor} t={t}" -> log`, resolver, + channel.Digest{Key: 100, DataType: telem.Float32T}, + channel.Digest{Key: 102, DataType: telem.Int32T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(102, telem.NewSeriesV[int32](7)) + h.Ingest(100, telem.NewSeriesV[float32](3.14)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("v=3.14 t=7")) + }) + + It("synthesizes a fmt$ function for an rf-prefixed multi-line format string preserving backslashes across newlines", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "sensor": {types.F32(), 100}, + "t": {types.I32(), 102}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + "sensor -> rf`path\\to: {sensor}\nt={t}` -> log", resolver, + channel.Digest{Key: 100, DataType: telem.Float32T}, + channel.Digest{Key: 102, DataType: telem.Int32T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(102, telem.NewSeriesV[int32](7)) + h.Ingest(100, telem.NewSeriesV[float32](3.14)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("path\\to: 3.14\nt=7")) + }) + + It("synthesizes a fmt$ function for an rf-prefixed format string preserving backslashes", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "sensor": {types.F32(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `sensor -> rf"path\to: {sensor}" -> log`, resolver, + channel.Digest{Key: 100, DataType: telem.Float32T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[float32](3.14)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal(`path\to: 3.14`)) + }) + + It("synthesizes a fmt$ function for a multi-line format string with placeholders across newlines", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "sensor": {types.F32(), 100}, + "t": {types.I32(), 102}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + "sensor -> f`v={sensor}\nt={t}` -> log", resolver, + channel.Digest{Key: 100, DataType: telem.Float32T}, + channel.Digest{Key: 102, DataType: telem.Int32T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(102, telem.NewSeriesV[int32](7)) + h.Ingest(100, telem.NewSeriesV[float32](3.14)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("v=3.14\nt=7")) + }) + + It("flows a literal raw string (no placeholders) without synthesizing a function", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "trig": {types.U8(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `trig -> f"static" -> log`, resolver, + channel.Digest{Key: 100, DataType: telem.Uint8T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[uint8](1)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal("static")) + }) + + It("flows a raw format string with only doubled-brace literals (no placeholders)", func(ctx SpecContext) { + resolver := channelSymbols(map[string]channelDef{ + "trig": {types.U8(), 100}, + "log": {types.String(), 101}, + }) + h := newRuntimeHarness(ctx, + `trig -> rf"C:\logs\{{abc}}.txt" -> log`, resolver, + channel.Digest{Key: 100, DataType: telem.Uint8T}, + channel.Digest{Key: 101, DataType: telem.StringT}, + ) + defer h.Close(ctx) + h.Ingest(100, telem.NewSeriesV[uint8](1)) + for i := 0; i < 5; i++ { + h.Tick(ctx, telem.Millisecond) + h.channelState.ClearReads() + } + out, _ := h.Flush() + Expect(lastString(out, 101)).To(Equal(`C:\logs\{abc}.txt`)) + }) + }) + +}) diff --git a/arc/go/formatter/formatter_test.go b/arc/go/formatter/formatter_test.go index fce22359b7..c89f3741e9 100644 --- a/arc/go/formatter/formatter_test.go +++ b/arc/go/formatter/formatter_test.go @@ -249,19 +249,22 @@ var _ = Describe("Formatter", func() { Entry("preserve string escapes", `x := "hello\nworld"`, "x := \"hello\\nworld\"\n"), ) - DescribeTable("Raw String Literals", + DescribeTable("Raw, Multi-Line, and Format String Literals", func(input, expected string) { Expect(formatter.Format(input)).To(Equal(expected)) }, - Entry("bare raw literal", "`hello`", "`hello`\n"), - Entry("simple raw in assignment", "x := `hello`", "x := `hello`\n"), - Entry("tight raw in assignment adds spaces", "x:=`hi`", "x := `hi`\n"), - Entry("raw in func call", "log(`hi`)", "log(`hi`)\n"), + Entry("bare format literal", `f"hello"`, "f\"hello\"\n"), + Entry("simple raw in assignment", `x := r"hello"`, "x := r\"hello\"\n"), + Entry("tight raw in assignment adds spaces", `x:=r"hi"`, "x := r\"hi\"\n"), + Entry("format string in func call", `log(f"hi")`, "log(f\"hi\")\n"), Entry("multi-line preserves newlines", "x := `a\nb`", "x := `a\nb`\n"), Entry("multi-line preserves indentation", "x := `\n indent`", "x := `\n indent`\n"), - Entry("embedded double quotes preserved", "x := `say \"hi\"`", "x := `say \"hi\"`\n"), - Entry("spacing raw next to identifier", "x:=`y`", "x := `y`\n"), - Entry("escaped backtick preserved", "x := `say \\`hi\\``", "x := `say \\`hi\\``\n"), + Entry("embedded escaped quotes preserved", `x := "say \"hi\""`, "x := \"say \\\"hi\\\"\"\n"), + Entry("spacing format next to identifier", `x:=f"y"`, "x := f\"y\"\n"), + Entry("rf prefix preserved", `x := rf"path: {p}"`, "x := rf\"path: {p}\"\n"), + Entry("format multi-line with placeholder", "x := f`a={p}\nb={q}`", "x := f`a={p}\nb={q}`\n"), + Entry("raw multi-line preserved", "x := r`a\\nb\nc`", "x := r`a\\nb\nc`\n"), + Entry("rf multi-line preserved", "x := rf`path\\to\n{p}`", "x := rf`path\\to\n{p}`\n"), ) DescribeTable("Global Constants", diff --git a/arc/go/formatter/printer.go b/arc/go/formatter/printer.go index bd158535e3..1aa1f2d71f 100644 --- a/arc/go/formatter/printer.go +++ b/arc/go/formatter/printer.go @@ -1119,7 +1119,7 @@ func (p *printer) isType(tokType int) bool { func (p *printer) isLiteral(tokType int) bool { switch tokType { case parser.ArcLexerINTEGER_LITERAL, parser.ArcLexerFLOAT_LITERAL, parser.ArcLexerSTR_LITERAL, - parser.ArcLexerSTR_LITERAL_RAW: + parser.ArcLexerSTR_LITERAL_MULTI: return true } return false diff --git a/arc/go/literal/fmt_fuzz_test.go b/arc/go/literal/fmt_fuzz_test.go new file mode 100644 index 0000000000..bf77317cd7 --- /dev/null +++ b/arc/go/literal/fmt_fuzz_test.go @@ -0,0 +1,101 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package literal_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/synnaxlabs/arc/literal" + "github.com/synnaxlabs/arc/types" +) + +const ( + catInt = "int" + catFloat = "float" + catStr = "str" +) + +type fuzzType struct { + name string + t types.Type + dummy any + category string +} + +var fuzzTypes = []fuzzType{ + {"i8", types.I8(), int64(0), catInt}, + {"i16", types.I16(), int64(0), catInt}, + {"i32", types.I32(), int64(0), catInt}, + {"i64", types.I64(), int64(0), catInt}, + {"u8", types.U8(), uint64(0), catInt}, + {"u16", types.U16(), uint64(0), catInt}, + {"u32", types.U32(), uint64(0), catInt}, + {"u64", types.U64(), uint64(0), catInt}, + {"f32", types.F32(), float64(0), catFloat}, + {"f64", types.F64(), float64(0), catFloat}, + {"string", types.String(), "", catStr}, +} + +// FuzzValidateSpec asserts the following invariants for any (spec, type) pair: +// +// 1. ValidateSpec never panics. +// 2. If a non-empty spec is accepted, fmt.Sprintf("%"+spec, dummy) must not +// contain "%!" (i.e. Go's fmt agrees it is a real spec for the type). +// 3. Accepted specs never contain a globally-blacklisted verb (v, T, U). +// 4. Accepted specs for string types never contain x or X. +// 5. Accepted specs for integer types never contain q. +// +// Run as `go test -fuzz=FuzzValidateSpec` for randomized exploration; plain +// `go test` exercises the seed corpus, which already covers the documented +// flag/width/precision/verb combinations and known malformed shapes. +func FuzzValidateSpec(f *testing.F) { + seeds := []string{ + "", "d", "x", "X", "b", "o", "O", "c", "f", "e", "E", "g", "G", "s", "q", + "v", "T", "U", "z", + "+d", "-d", "#x", "#b", "#o", " d", "0d", + "5d", "05d", "+05d", "-5d", "-5s", "5s", "20s", "-20q", + ".2f", "5.2f", ".0f", "+f", "+08.2f", "#06x", "6.2f", ".10g", + "5", ".2", ".", "5+d", "f.2", "d5", "sx", ".2-5f", + "%", "{", "}", "abc", "+++d", "..2f", + } + for _, spec := range seeds { + for i := range fuzzTypes { + f.Add(spec, uint8(i)) + } + } + f.Fuzz(func(t *testing.T, spec string, kind uint8) { + ft := fuzzTypes[int(kind)%len(fuzzTypes)] + err := literal.FmtStrValidateSpec(spec, ft.t) + if err != nil { + return + } + if spec == "" { + return + } + out := fmt.Sprintf("%"+spec, ft.dummy) + if strings.Contains(out, "%!") { + t.Fatalf("ValidateSpec accepted %q for %s but Go fmt rejects: %q", + spec, ft.name, out) + } + if strings.ContainsAny(spec, "vTU") { + t.Fatalf("ValidateSpec accepted blacklisted verb in %q for %s", + spec, ft.name) + } + if ft.category == catStr && strings.ContainsAny(spec, "xX") { + t.Fatalf("ValidateSpec accepted string-blocked verb in %q", spec) + } + if ft.category == catInt && strings.ContainsAny(spec, "q") { + t.Fatalf("ValidateSpec accepted int-blocked verb q in %q for %s", + spec, ft.name) + } + }) +} diff --git a/arc/go/literal/fmt_str.go b/arc/go/literal/fmt_str.go new file mode 100644 index 0000000000..461d4b785e --- /dev/null +++ b/arc/go/literal/fmt_str.go @@ -0,0 +1,322 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package literal + +import ( + "regexp" + "slices" + "strconv" + "strings" + + "github.com/synnaxlabs/arc/types" + "github.com/synnaxlabs/x/errors" +) + +// FmtStrSegment is one piece of a parsed format string. +type FmtStrSegment struct { + // Literal text, or for placeholders, the expression source. + Text string + // Optional format spec after `:` in a placeholder. + Spec string + // True if this segment was a `{...}` placeholder. + IsPlaceholder bool + // Start byte offset in the body. + Start int + // End byte offset in the body, exclusive. + End int + // Body byte offset of `:`, or -1 if no spec. + SpecOffset int +} + +// FmtStrHasPlaceholder reports whether any segment is a placeholder. +func FmtStrHasPlaceholder(segs []FmtStrSegment) bool { + return slices.ContainsFunc(segs, func(s FmtStrSegment) bool { return s.IsPlaceholder }) +} + +// StringFlags carries the optional r/f prefix and the quote style of a string +// literal. Raw skips standard escape processing; Format opts into {expr} +// placeholders; Multi means the literal was backtick-delimited. +type StringFlags struct { + Raw bool + Format bool + Multi bool +} + +// StripQuotes peels the optional r/f prefix and surrounding quote delimiters +// from a string literal token, returning the inner body. ok=false when text is +// malformed; well-formed tokens from the lexer should always succeed. +func StripQuotes(text string) (body string, flags StringFlags, ok bool) { + rest := text + for i := 0; i < 2 && len(rest) > 0 && (rest[0] == 'r' || rest[0] == 'f'); i++ { + switch rest[0] { + case 'r': + if flags.Raw { + return "", StringFlags{}, false + } + flags.Raw = true + case 'f': + if flags.Format { + return "", StringFlags{}, false + } + flags.Format = true + } + rest = rest[1:] + } + if len(rest) >= 2 && rest[0] == '`' && rest[len(rest)-1] == '`' { + flags.Multi = true + return rest[1 : len(rest)-1], flags, true + } + if len(rest) >= 2 && rest[0] == '"' && rest[len(rest)-1] == '"' { + return rest[1 : len(rest)-1], flags, true + } + return "", StringFlags{}, false +} + +// UnescapeString applies the standard Arc escape table to body. The delimiter +// escape is asymmetric: single-line strings recognize \", multi-line strings +// recognize \`; the other passes through verbatim. Unrecognized escapes also +// pass through verbatim. Literal-brace escapes ({{ and }}) are handled by +// FmtStrParse. Errors only on a trailing backslash or an incomplete \uXXXX escape. +func UnescapeString(body string, multi bool) (string, error) { + var b strings.Builder + b.Grow(len(body)) + for i := 0; i < len(body); { + c := body[i] + if c != '\\' { + b.WriteByte(c) + i++ + continue + } + if i+1 >= len(body) { + return "", errors.New("trailing backslash in string literal") + } + next := body[i+1] + switch { + case next == 'b': + b.WriteByte('\b') + case next == 't': + b.WriteByte('\t') + case next == 'n': + b.WriteByte('\n') + case next == 'f': + b.WriteByte('\f') + case next == 'r': + b.WriteByte('\r') + case next == '"' && !multi: + b.WriteByte('"') + case next == '`' && multi: + b.WriteByte('`') + case next == '\\': + b.WriteByte('\\') + case next == 'u': + if i+6 > len(body) { + return "", errors.New(`incomplete \u escape in string literal`) + } + cp, err := strconv.ParseUint(body[i+2:i+6], 16, 32) + if err != nil { + return "", errors.Newf(`invalid \u escape %q in string literal`, body[i:i+6]) + } + b.WriteRune(rune(cp)) + i += 6 + continue + default: + b.WriteByte('\\') + b.WriteByte(next) + } + i += 2 + } + return b.String(), nil +} + +// FmtStrParse splits a format-string body into ordered segments at `{...}` +// placeholders. `{{` escapes to a literal `{` and `}}` to a literal `}`; a +// bare `}` outside a placeholder is plain text. +func FmtStrParse(body string) ([]FmtStrSegment, error) { + var segments []FmtStrSegment + pos := 0 + for pos < len(body) { + open, text := scanText(body, pos) + if text != "" { + segments = append(segments, FmtStrSegment{ + Text: text, + Start: pos, + End: open, + SpecOffset: -1, + }) + } + if open == len(body) { + return segments, nil + } + close, err := findPlaceholderClose(body, open) + if err != nil { + return nil, err + } + expr := body[open+1 : close] + exprPart, spec, err := splitSpec(expr) + if err != nil { + return nil, err + } + specOffset := -1 + if spec != "" { + specOffset = open + 1 + len(exprPart) + } + segments = append(segments, FmtStrSegment{ + Text: exprPart, + Spec: spec, + IsPlaceholder: true, + Start: open, + End: close + 1, + SpecOffset: specOffset, + }) + pos = close + 1 + } + return segments, nil +} + +func scanText(body string, pos int) (int, string) { + var b strings.Builder + for i := pos; i < len(body); i++ { + c := body[i] + if c == '{' { + if i+1 < len(body) && body[i+1] == '{' { + b.WriteByte('{') + i++ + continue + } + return i, b.String() + } + if c == '}' && i+1 < len(body) && body[i+1] == '}' { + b.WriteByte('}') + i++ + continue + } + b.WriteByte(c) + } + return len(body), b.String() +} + +func findPlaceholderClose(body string, open int) (int, error) { + depth := 1 + for i := open + 1; i < len(body); i++ { + switch body[i] { + case '{': + depth++ + case '}': + depth-- + if depth == 0 { + return i, nil + } + } + } + return 0, errors.New("unmatched '{'") +} + +// splitSpec splits a placeholder body on the last `:` at brace and bracket +// depth 0, yielding (expr, spec). Colons inside nested `{...}` (struct +// literals) or `[...]` (index/slice expressions) are skipped. +func splitSpec(body string) (expr, spec string, err error) { + idx := -1 + braceDepth := 0 + bracketDepth := 0 + for i := len(body) - 1; i >= 0; i-- { + switch body[i] { + case '}': + braceDepth++ + case '{': + braceDepth-- + case ']': + bracketDepth++ + case '[': + bracketDepth-- + case ':': + if braceDepth == 0 && bracketDepth == 0 { + idx = i + } + } + if idx >= 0 { + break + } + } + if idx < 0 { + return body, "", nil + } + expr = body[:idx] + spec = body[idx+1:] + if expr == "" { + return "", "", errors.New("placeholder must contain an expression before ':'") + } + if spec == "" { + return "", "", errors.New("placeholder format spec after ':' is empty") + } + return expr, spec, nil +} + +// specShape enforces the canonical anatomy [flags][width][.precision][verb]. +var specShape = regexp.MustCompile(`^[#+\- 0]*\d*(\.\d+)?[a-zA-Z]$`) + +var ( + stringKinds = []types.Kind{types.KindString} + intKinds = []types.Kind{ + types.KindI8, types.KindI16, types.KindI32, types.KindI64, + types.KindU8, types.KindU16, types.KindU32, types.KindU64, + types.KindIntegerConstant, + } + floatKinds = []types.Kind{ + types.KindF32, types.KindF64, + types.KindFloatConstant, types.KindNumericConstant, + types.KindExactIntegerFloatConstant, + } + numericKinds = slices.Concat(intKinds, floatKinds) + formattableKinds = slices.Concat(stringKinds, numericKinds) +) + +// verbAllowedKinds maps each supported format verb to the type kinds it can +// format. To add a verb later (e.g., a timestamp verb), add an entry here. +var verbAllowedKinds = map[byte][]types.Kind{ + 's': stringKinds, + 'q': stringKinds, + 'b': intKinds, + 'c': intKinds, + 'd': intKinds, + 'o': intKinds, + 'O': intKinds, + 'x': intKinds, + 'X': intKinds, + 'e': floatKinds, + 'E': floatKinds, + 'f': floatKinds, + 'g': floatKinds, + 'G': floatKinds, +} + +// FmtStrValidateSpec reports an error if spec is not a supported verb for t, +// or if t is not a formattable kind. +func FmtStrValidateSpec(spec string, t types.Type) error { + if spec == "" { + return nil + } + if t.Kind == types.KindVariable { + if t.Constraint == nil { + return errors.Newf("cannot format type %s", t) + } + return FmtStrValidateSpec(spec, *t.Constraint) + } + if !slices.Contains(formattableKinds, t.Kind) { + return errors.Newf("cannot format type %s", t) + } + if !specShape.MatchString(spec) { + return errors.Newf("invalid format spec %q for type %s", spec, t) + } + allowed, ok := verbAllowedKinds[spec[len(spec)-1]] + if !ok || !slices.Contains(allowed, t.Kind) { + return errors.Newf("invalid format spec %q for type %s", spec, t) + } + return nil +} diff --git a/arc/go/literal/fmt_test.go b/arc/go/literal/fmt_test.go new file mode 100644 index 0000000000..a2b2cd581d --- /dev/null +++ b/arc/go/literal/fmt_test.go @@ -0,0 +1,456 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +package literal_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/synnaxlabs/arc/literal" + "github.com/synnaxlabs/arc/types" + . "github.com/synnaxlabs/x/testutil" +) + +var _ = Describe("Parse", func() { + DescribeTable("valid bodies", + func(body string, expected []literal.FmtStrSegment) { + Expect(MustSucceed(literal.FmtStrParse(body))).To(Equal(expected)) + }, + Entry("empty body", "", []literal.FmtStrSegment(nil)), + Entry("plain literal", "hello", + []literal.FmtStrSegment{ + {Text: "hello", Start: 0, End: 5, SpecOffset: -1}, + }), + Entry("literal with newlines", "line1\nline2\nline3", + []literal.FmtStrSegment{ + {Text: "line1\nline2\nline3", Start: 0, End: 17, SpecOffset: -1}, + }), + Entry("literal with tabs and CR", "a\tb\rc", + []literal.FmtStrSegment{ + {Text: "a\tb\rc", Start: 0, End: 5, SpecOffset: -1}, + }), + Entry("literal with unicode", "hΓ©llo δΈ–η•Œ πŸš€", + []literal.FmtStrSegment{ + {Text: "hΓ©llo δΈ–η•Œ πŸš€", Start: 0, End: 18, SpecOffset: -1}, + }), + Entry("literal with bare percent", "50% off", + []literal.FmtStrSegment{ + {Text: "50% off", Start: 0, End: 7, SpecOffset: -1}, + }), + Entry("placeholder only", "{x}", + []literal.FmtStrSegment{ + {Text: "x", IsPlaceholder: true, Start: 0, End: 3, SpecOffset: -1}, + }), + Entry("literal then placeholder", "pre {x}", []literal.FmtStrSegment{ + {Text: "pre ", Start: 0, End: 4, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 4, End: 7, SpecOffset: -1}, + }), + Entry("placeholder then literal", "{x} post", []literal.FmtStrSegment{ + {Text: "x", IsPlaceholder: true, Start: 0, End: 3, SpecOffset: -1}, + {Text: " post", Start: 3, End: 8, SpecOffset: -1}, + }), + Entry("literal surrounding placeholder", "pre {x} post", + []literal.FmtStrSegment{ + {Text: "pre ", Start: 0, End: 4, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 4, End: 7, SpecOffset: -1}, + {Text: " post", Start: 7, End: 12, SpecOffset: -1}, + }), + Entry("two placeholders separated by literal", "{a} {b}", + []literal.FmtStrSegment{ + {Text: "a", IsPlaceholder: true, Start: 0, End: 3, SpecOffset: -1}, + {Text: " ", Start: 3, End: 4, SpecOffset: -1}, + {Text: "b", IsPlaceholder: true, Start: 4, End: 7, SpecOffset: -1}, + }), + Entry("two adjacent placeholders", "{a}{b}", + []literal.FmtStrSegment{ + {Text: "a", IsPlaceholder: true, Start: 0, End: 3, SpecOffset: -1}, + {Text: "b", IsPlaceholder: true, Start: 3, End: 6, SpecOffset: -1}, + }), + Entry("three placeholders mixed with literal", "x={x} y={y} z={z}", + []literal.FmtStrSegment{ + {Text: "x=", Start: 0, End: 2, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 2, End: 5, SpecOffset: -1}, + {Text: " y=", Start: 5, End: 8, SpecOffset: -1}, + {Text: "y", IsPlaceholder: true, Start: 8, End: 11, SpecOffset: -1}, + {Text: " z=", Start: 11, End: 14, SpecOffset: -1}, + {Text: "z", IsPlaceholder: true, Start: 14, End: 17, SpecOffset: -1}, + }), + Entry("placeholder spanning newlines in surrounding text", + "line1\n{x}\nline2", + []literal.FmtStrSegment{ + {Text: "line1\n", Start: 0, End: 6, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 6, End: 9, SpecOffset: -1}, + {Text: "\nline2", Start: 9, End: 15, SpecOffset: -1}, + }), + Entry("placeholder with float spec", "{x:.2f}", + []literal.FmtStrSegment{ + {Text: "x", Spec: ".2f", IsPlaceholder: true, Start: 0, End: 7, SpecOffset: 2}, + }), + Entry("placeholder with integer spec", "{n:d}", + []literal.FmtStrSegment{ + {Text: "n", Spec: "d", IsPlaceholder: true, Start: 0, End: 5, SpecOffset: 2}, + }), + Entry("placeholder with padded integer spec", "{n:05d}", + []literal.FmtStrSegment{ + {Text: "n", Spec: "05d", IsPlaceholder: true, Start: 0, End: 7, SpecOffset: 2}, + }), + Entry("placeholder with arithmetic expression", "{a + b}", + []literal.FmtStrSegment{ + {Text: "a + b", IsPlaceholder: true, Start: 0, End: 7, SpecOffset: -1}, + }), + Entry("placeholder with rightmost colon splitting expr from spec", + "{a:b:.2f}", + []literal.FmtStrSegment{ + {Text: "a:b", Spec: ".2f", IsPlaceholder: true, Start: 0, End: 9, SpecOffset: 4}, + }), + Entry("placeholder with function call", "{len(x)}", + []literal.FmtStrSegment{ + {Text: "len(x)", IsPlaceholder: true, Start: 0, End: 8, SpecOffset: -1}, + }), + Entry("placeholder with member access", "{a.b}", + []literal.FmtStrSegment{ + {Text: "a.b", IsPlaceholder: true, Start: 0, End: 5, SpecOffset: -1}, + }), + Entry("multiple placeholders each with spec", + "a={a:.2f} b={b:d}", + []literal.FmtStrSegment{ + {Text: "a=", Start: 0, End: 2, SpecOffset: -1}, + {Text: "a", Spec: ".2f", IsPlaceholder: true, Start: 2, End: 9, SpecOffset: 4}, + {Text: " b=", Start: 9, End: 12, SpecOffset: -1}, + {Text: "b", Spec: "d", IsPlaceholder: true, Start: 12, End: 17, SpecOffset: 14}, + }), + Entry(`doubled opening brace is literal`, `{{`, + []literal.FmtStrSegment{ + {Text: "{", Start: 0, End: 2, SpecOffset: -1}, + }), + Entry(`doubled closing brace is literal`, `}}`, + []literal.FmtStrSegment{ + {Text: "}", Start: 0, End: 2, SpecOffset: -1}, + }), + Entry(`bare closing brace is literal`, `}`, + []literal.FmtStrSegment{ + {Text: "}", Start: 0, End: 1, SpecOffset: -1}, + }), + Entry(`doubled open with doubled close`, `{{ }}`, + []literal.FmtStrSegment{ + {Text: "{ }", Start: 0, End: 5, SpecOffset: -1}, + }), + Entry(`doubled braces around literal`, `{{hello}}`, + []literal.FmtStrSegment{ + {Text: "{hello}", Start: 0, End: 9, SpecOffset: -1}, + }), + Entry(`doubled braces around placeholder`, `{{{x}}}`, + []literal.FmtStrSegment{ + {Text: "{", Start: 0, End: 2, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 2, End: 5, SpecOffset: -1}, + {Text: "}", Start: 5, End: 7, SpecOffset: -1}, + }), + Entry(`doubled braces mixed with placeholder`, + `pre {{ {x} }} post`, + []literal.FmtStrSegment{ + {Text: "pre { ", Start: 0, End: 7, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 7, End: 10, SpecOffset: -1}, + {Text: " } post", Start: 10, End: 18, SpecOffset: -1}, + }), + Entry(`literal backslash before non-brace`, `a\nb`, + []literal.FmtStrSegment{ + {Text: `a\nb`, Start: 0, End: 4, SpecOffset: -1}, + }), + Entry(`literal backslash before close brace`, `a\}b`, + []literal.FmtStrSegment{ + {Text: `a\}b`, Start: 0, End: 4, SpecOffset: -1}, + }), + Entry(`placeholder adjacent to bare close`, `{x}}`, + []literal.FmtStrSegment{ + {Text: "x", IsPlaceholder: true, Start: 0, End: 3, SpecOffset: -1}, + {Text: "}", Start: 3, End: 4, SpecOffset: -1}, + }), + Entry(`doubled brace spanning newlines`, + "line1 {{\nline2}}", + []literal.FmtStrSegment{ + {Text: "line1 {\nline2}", Start: 0, End: 16, SpecOffset: -1}, + }), + Entry("bare close prefix", "}foo", + []literal.FmtStrSegment{ + {Text: "}foo", Start: 0, End: 4, SpecOffset: -1}, + }), + Entry("bare close suffix", "foo}", + []literal.FmtStrSegment{ + {Text: "foo}", Start: 0, End: 4, SpecOffset: -1}, + }), + Entry("bare close in middle", "foo}bar", + []literal.FmtStrSegment{ + {Text: "foo}bar", Start: 0, End: 7, SpecOffset: -1}, + }), + Entry("empty placeholder", "{}", + []literal.FmtStrSegment{ + {Text: "", IsPlaceholder: true, Start: 0, End: 2, SpecOffset: -1}, + }), + Entry("empty placeholder surrounded by text", "pre {} post", + []literal.FmtStrSegment{ + {Text: "pre ", Start: 0, End: 4, SpecOffset: -1}, + {Text: "", IsPlaceholder: true, Start: 4, End: 6, SpecOffset: -1}, + {Text: " post", Start: 6, End: 11, SpecOffset: -1}, + }), + Entry("placeholder with balanced nested braces", "{f({a: 1})}", + []literal.FmtStrSegment{ + {Text: "f({a: 1})", IsPlaceholder: true, Start: 0, End: 11, SpecOffset: -1}, + }), + Entry("placeholder with balanced nested braces and spec", + "{f({a: 1}):d}", + []literal.FmtStrSegment{ + { + Text: "f({a: 1})", Spec: "d", IsPlaceholder: true, + Start: 0, End: 13, SpecOffset: 10, + }, + }), + Entry("placeholder with index expression", "{arr[0]}", + []literal.FmtStrSegment{ + {Text: "arr[0]", IsPlaceholder: true, Start: 0, End: 8, SpecOffset: -1}, + }), + Entry("placeholder with slice expression", "{arr[0:5]}", + []literal.FmtStrSegment{ + {Text: "arr[0:5]", IsPlaceholder: true, Start: 0, End: 10, SpecOffset: -1}, + }), + Entry("placeholder with slice expression and spec", "{arr[0:5]:s}", + []literal.FmtStrSegment{ + { + Text: "arr[0:5]", Spec: "s", IsPlaceholder: true, + Start: 0, End: 12, SpecOffset: 9, + }, + }), + // Adjacency tests: a literal backslash followed by a placeholder. The + // expected behavior (matching Python's rf"...") is that the backslash + // stays literal and the {expr} is interpolated. This is required for + // Windows-style paths like rf"C:\logs\{name}.txt" to work. + Entry(`backslash immediately before placeholder`, `\{x}`, + []literal.FmtStrSegment{ + {Text: `\`, Start: 0, End: 1, SpecOffset: -1}, + {Text: "x", IsPlaceholder: true, Start: 1, End: 4, SpecOffset: -1}, + }), + Entry(`Windows path with placeholder after final backslash`, + `C:\logs\{name}.txt`, + []literal.FmtStrSegment{ + {Text: `C:\logs\`, Start: 0, End: 8, SpecOffset: -1}, + {Text: "name", IsPlaceholder: true, Start: 8, End: 14, SpecOffset: -1}, + {Text: ".txt", Start: 14, End: 18, SpecOffset: -1}, + }), + ) + + DescribeTable("error cases", + func(body, errSubstr string) { + Expect(literal.FmtStrParse(body)).Error().To(MatchError(ContainSubstring(errSubstr))) + }, + Entry("lone opening brace", "{", "unmatched '{'"), + Entry("opening brace with body, no close", "{foo", "unmatched '{'"), + Entry("opening brace with another opening inside", "{foo{bar}", + "unmatched '{'"), + Entry("opening brace before valid placeholder", "{ {x}", + "unmatched '{'"), + Entry("opening brace at end of literal", "literal {", + "unmatched '{'"), + Entry("placeholder starting with format spec", "{:.2f}", + "expression before ':'"), + Entry("placeholder with empty spec after colon", "{x:}", + "format spec after ':' is empty"), + ) +}) + +var _ = Describe("StripQuotes", func() { + DescribeTable("strips quotes and peels prefix", + func(input, expectedBody string, expectedFlags literal.StringFlags) { + body, flags, ok := literal.StripQuotes(input) + Expect(ok).To(BeTrue()) + Expect(body).To(Equal(expectedBody)) + Expect(flags).To(Equal(expectedFlags)) + }, + Entry("plain double-quoted", `"hello"`, "hello", literal.StringFlags{}), + Entry("empty double-quoted", `""`, "", literal.StringFlags{}), + Entry("plain backtick", "`hello`", "hello", + literal.StringFlags{Multi: true}), + Entry("empty backtick", "``", "", + literal.StringFlags{Multi: true}), + Entry("backtick with newline", "`a\nb`", "a\nb", + literal.StringFlags{Multi: true}), + Entry("backtick body containing double quote", "`a\"b`", `a"b`, + literal.StringFlags{Multi: true}), + Entry("raw double-quoted", `r"path"`, "path", + literal.StringFlags{Raw: true}), + Entry("raw backtick", "r`path`", "path", + literal.StringFlags{Raw: true, Multi: true}), + Entry("format double-quoted", `f"hi {x}"`, "hi {x}", + literal.StringFlags{Format: true}), + Entry("format backtick", "f`hi {x}`", "hi {x}", + literal.StringFlags{Format: true, Multi: true}), + Entry("rf double-quoted", `rf"hi"`, "hi", + literal.StringFlags{Raw: true, Format: true}), + Entry("fr double-quoted", `fr"hi"`, "hi", + literal.StringFlags{Raw: true, Format: true}), + Entry("rf backtick", "rf`hi`", "hi", + literal.StringFlags{Raw: true, Format: true, Multi: true}), + Entry("body with embedded escape sequence", `"a\nb"`, `a\nb`, + literal.StringFlags{}), + ) + + DescribeTable("rejects malformed input", + func(input string) { + body, flags, ok := literal.StripQuotes(input) + Expect(ok).To(BeFalse()) + Expect(body).To(BeEmpty()) + Expect(flags).To(Equal(literal.StringFlags{})) + }, + Entry("empty string", ""), + Entry("single quote", `"`), + Entry("missing leading quote", `hi"`), + Entry("missing trailing quote", `"hi`), + Entry("plain text no delimiters", "hello"), + Entry("single backtick", "`"), + Entry("duplicate r prefix", `rr"hi"`), + Entry("duplicate f prefix", `ff"hi"`), + ) +}) + +var _ = Describe("HasPlaceholder", func() { + It("returns true when any segment is a placeholder", func() { + segs := MustSucceed(literal.FmtStrParse("hello {x}")) + Expect(literal.FmtStrHasPlaceholder(segs)).To(BeTrue()) + }) + + It("returns false when no segment is a placeholder", func() { + segs := MustSucceed(literal.FmtStrParse("plain literal")) + Expect(literal.FmtStrHasPlaceholder(segs)).To(BeFalse()) + }) + + It("returns false for an empty segment slice", func() { + Expect(literal.FmtStrHasPlaceholder(nil)).To(BeFalse()) + }) +}) + +var _ = Describe("ValidateSpec", func() { + type namedType struct { + name string + t types.Type + } + intTypes := []namedType{ + {"i8", types.I8()}, {"i16", types.I16()}, + {"i32", types.I32()}, {"i64", types.I64()}, + {"u8", types.U8()}, {"u16", types.U16()}, + {"u32", types.U32()}, {"u64", types.U64()}, + } + floatTypes := []namedType{{"f32", types.F32()}, {"f64", types.F64()}} + stringType := namedType{"string", types.String()} + + var validArgs []any + validArgs = append(validArgs, func(spec string, t types.Type) { + Expect(literal.FmtStrValidateSpec(spec, t)).To(Succeed()) + }) + // Integer verbs across every integer type. + for _, verb := range []string{"d", "b", "o", "O", "x", "X", "c"} { + for _, it := range intTypes { + validArgs = append(validArgs, Entry(verb+" on "+it.name, verb, it.t)) + } + } + // Float verbs across every float type. + for _, verb := range []string{"f", "e", "E", "g", "G"} { + for _, ft := range floatTypes { + validArgs = append(validArgs, Entry(verb+" on "+ft.name, verb, ft.t)) + } + } + // String verbs. + for _, verb := range []string{"s", "q"} { + validArgs = append(validArgs, + Entry(verb+" on "+stringType.name, verb, stringType.t)) + } + // Flags, width, precision, and constants. + validArgs = append(validArgs, + Entry("padded decimal on i32", "05d", types.I32()), + Entry("signed decimal on i32", "+d", types.I32()), + Entry("decimal on integer constant", "d", types.IntegerConstraint()), + Entry("hex on integer constant", "05x", types.IntegerConstraint()), + Entry("float on float constant", ".2f", types.FloatConstraint()), + Entry("empty spec on string skips check", "", types.String()), + ) + DescribeTable("valid specs", validArgs...) + + nonIntTypes := append(append([]namedType{}, floatTypes...), stringType) + nonFloatTypes := append(append([]namedType{}, intTypes...), stringType) + nonStringTypes := append(append([]namedType{}, intTypes...), floatTypes...) + allTypes := append(append(append([]namedType{}, intTypes...), floatTypes...), stringType) + + var invalidArgs []any + invalidArgs = append(invalidArgs, func(spec string, t types.Type, errSubstr string) { + Expect(literal.FmtStrValidateSpec(spec, t)). + To(MatchError(ContainSubstring(errSubstr))) + }) + // Integer-only verbs rejected on every non-integer type. + for _, verb := range []string{"d", "o", "O", "c", "b", "x", "X"} { + for _, nt := range nonIntTypes { + invalidArgs = append(invalidArgs, + Entry(verb+" on "+nt.name, verb, nt.t, "invalid format spec")) + } + } + // Float verbs rejected on every non-float type. + for _, verb := range []string{"f", "e", "E", "g", "G"} { + for _, nt := range nonFloatTypes { + invalidArgs = append(invalidArgs, + Entry(verb+" on "+nt.name, verb, nt.t, "invalid format spec")) + } + } + // String verbs rejected on every non-string type. + for _, verb := range []string{"s", "q"} { + for _, nt := range nonStringTypes { + invalidArgs = append(invalidArgs, + Entry(verb+" on "+nt.name, verb, nt.t, "invalid format spec")) + } + } + // Blacklisted verbs rejected on every type. + for _, verb := range []string{"v", "T", "U"} { + for _, nt := range allTypes { + invalidArgs = append(invalidArgs, + Entry(verb+" on "+nt.name, verb, nt.t, "invalid format spec")) + } + } + // Spec-shape and malformed-spec error cases. + invalidArgs = append(invalidArgs, + Entry("unknown verb on int", "z", types.I32(), "invalid format spec"), + Entry("unknown verb on float", "z", types.F64(), "invalid format spec"), + Entry("blacklisted verb with flag on i32", "+v", types.I32(), "invalid format spec"), + Entry("blacklisted verb with width on f64", "5v", types.F64(), "invalid format spec"), + Entry("trailing chars after float verb", "f.2", types.F64(), "invalid format spec"), + Entry("trailing chars after integer verb", "d5", types.I32(), "invalid format spec"), + Entry("trailing chars after string verb", "sx", types.String(), "invalid format spec"), + Entry("precision before width", ".2-5f", types.F64(), "invalid format spec"), + Entry("flag after width", "5+d", types.I32(), "invalid format spec"), + Entry("missing verb", "5", types.I32(), "invalid format spec"), + Entry("only precision no verb", ".2", types.F64(), "invalid format spec"), + Entry("precision without digits", ".f", types.F64(), "invalid format spec"), + ) + DescribeTable("error cases", invalidArgs...) + + It("validates against the constraint of a constrained type variable", func() { + intConstraint := types.IntegerConstraint() + Expect(literal.FmtStrValidateSpec("d", types.Variable("T", &intConstraint))).To(Succeed()) + }) + + It("rejects a spec invalid for the variable's constraint", func() { + stringConstraint := types.String() + Expect(literal.FmtStrValidateSpec(".2f", types.Variable("T", &stringConstraint))). + To(MatchError(ContainSubstring("invalid format spec"))) + }) + + It("errors on an unconstrained type variable", func() { + Expect(literal.FmtStrValidateSpec("d", types.Variable("T", nil))). + To(MatchError(ContainSubstring("cannot format type"))) + }) + + It("errors on a non-formattable type kind", func() { + Expect(literal.FmtStrValidateSpec("d", types.Chan(types.I32()))). + To(MatchError(ContainSubstring("cannot format type"))) + }) +}) diff --git a/arc/go/literal/literal.go b/arc/go/literal/literal.go index 59f09b79dd..3118a5763f 100644 --- a/arc/go/literal/literal.go +++ b/arc/go/literal/literal.go @@ -12,6 +12,7 @@ package literal import ( "math" "strconv" + "strings" "github.com/synnaxlabs/arc/analyzer/units" "github.com/synnaxlabs/arc/parser" @@ -41,61 +42,50 @@ func Parse( if num := literal.NumericLiteral(); num != nil { return ParseNumeric(num, targetType) } - if str := literal.STR_LITERAL(); str != nil { + if str := parser.StringTerminal(literal); str != nil { return ParseString(str.GetText(), targetType) } - if str := literal.STR_LITERAL_RAW(); str != nil { - return ParseRawString(str.GetText(), targetType) - } if series := literal.SeriesLiteral(); series != nil { return ParsedValue{}, errors.New("series literals not supported for default values") } return ParsedValue{}, errors.New("unknown literal type") } -// ParseString parses a string literal and returns its value and type. -// It handles escape sequences according to the Arc grammar: -// - \b, \t, \n, \f, \r, \", \\ -// - \uXXXX (4-digit Unicode escape) -// The text parameter should include the surrounding double quotes. +// ParseString parses any of the eight string literal forms ("..." / `...` +// crossed with optional r/f/rf/fr prefix) and returns the cooked Go string. For +// format-prefixed strings without placeholders, {{ and }} are collapsed to +// literal braces; for format strings with placeholders the body is returned +// with placeholders intact so callers can run FmtStrParse themselves. func ParseString(text string, targetType types.Type) (ParsedValue, error) { if targetType.IsValid() && targetType.Kind != types.KindString { return ParsedValue{}, errors.Newf("cannot assign string to %s", targetType) } - unquoted, err := strconv.Unquote(text) - if err != nil { - return ParsedValue{}, errors.Wrapf(err, "invalid string literal: %s", text) - } - return ParsedValue{Value: unquoted, Type: types.String()}, nil -} - -// ParseRawString parses a raw string literal delimited by backticks and returns -// its value and type. The escape sequence \` produces a literal backtick. All -// other content (including embedded newlines and tabs) is preserved as-is. -func ParseRawString(text string, targetType types.Type) (ParsedValue, error) { - if targetType.IsValid() && targetType.Kind != types.KindString { - return ParsedValue{}, errors.Newf("cannot assign string to %s", targetType) + body, flags, ok := StripQuotes(text) + if !ok { + return ParsedValue{}, errors.Newf("invalid string literal: %s", text) } - if len(text) < 2 || text[0] != '`' || text[len(text)-1] != '`' { - return ParsedValue{}, errors.Newf("invalid raw string literal: %s", text) + value := body + if !flags.Raw { + var err error + value, err = UnescapeString(body, flags.Multi) + if err != nil { + return ParsedValue{}, errors.Wrapf(err, "invalid string literal %s", text) + } } - body := unescapeRawBackticks(text[1 : len(text)-1]) - return ParsedValue{Value: body, Type: types.String()}, nil -} - -// unescapeRawBackticks replaces every \` sequence with a literal backtick. -func unescapeRawBackticks(s string) string { - n := len(s) - var out []byte - for i := 0; i < n; i++ { - if s[i] == '\\' && i+1 < n && s[i+1] == '`' { - out = append(out, '`') - i++ - } else { - out = append(out, s[i]) + if flags.Format { + segments, err := FmtStrParse(value) + if err != nil { + return ParsedValue{}, errors.Wrapf(err, "invalid format string %s", text) + } + if !FmtStrHasPlaceholder(segments) { + var sb strings.Builder + for _, seg := range segments { + sb.WriteString(seg.Text) + } + value = sb.String() } } - return string(out) + return ParsedValue{Value: value, Type: types.String()}, nil } // ParseNumeric parses a numeric literal (integer or float, with optional unit suffix) diff --git a/arc/go/literal/literal_test.go b/arc/go/literal/literal_test.go index 6b49fe9bea..34e892c22a 100644 --- a/arc/go/literal/literal_test.go +++ b/arc/go/literal/literal_test.go @@ -325,72 +325,73 @@ var _ = Describe("Literal Parser", func() { }) }) - Describe("Raw string literals", func() { - DescribeTable("ParseRawString happy path", + Describe("Raw and multi-line string literals", func() { + DescribeTable("ParseString happy path", func(input string, target types.Type, expected string) { - parsed := MustSucceed(literal.ParseRawString(input, target)) + parsed := MustSucceed(literal.ParseString(input, target)) Expect(parsed.Value).To(Equal(expected)) Expect(parsed.Type).To(Equal(types.String())) }, - Entry("empty", "``", types.String(), ""), - Entry("simple", "`hello`", types.String(), "hello"), - Entry("with spaces", "`hello world`", types.String(), "hello world"), - Entry("double quotes inside", "`say \"hi\"`", types.String(), `say "hi"`), - Entry("backslash-n verbatim", "`a\\nb`", types.String(), `a\nb`), - Entry("backslash-t verbatim", "`col1\\tcol2`", types.String(), `col1\tcol2`), - Entry("backslash-quote verbatim", "`\\\"`", types.String(), `\"`), - Entry("real newline preserved", "`line1\nline2`", types.String(), "line1\nline2"), - Entry("three-line literal", "`a\nb\nc`", types.String(), "a\nb\nc"), - Entry("indentation preserved", "`a\n b`", types.String(), "a\n b"), - Entry("tab char preserved", "`a\tb`", types.String(), "a\tb"), - Entry("unicode", "`Β°C`", types.String(), "Β°C"), - Entry("escaped backtick", "`say \\`hi\\``", types.String(), "say `hi`"), - Entry("escaped backtick at start", "`\\`hello`", types.String(), "`hello"), - Entry("escaped backtick at end", "`hello\\``", types.String(), "hello`"), - Entry("only escaped backtick", "`\\``", types.String(), "`"), - Entry("adjacent escaped backticks", "`\\`\\``", types.String(), "``"), - Entry("no target type infers string", "`hi`", types.Type{}, "hi"), + Entry("empty raw", `r""`, types.String(), ""), + Entry("simple raw", `r"hello"`, types.String(), "hello"), + Entry("raw with spaces", `r"hello world"`, types.String(), "hello world"), + Entry("raw backslash-n verbatim", `r"a\nb"`, types.String(), `a\nb`), + Entry("raw backslash-t verbatim", `r"col1\tcol2"`, types.String(), `col1\tcol2`), + Entry("raw escaped quote preserves backslash", `r"\""`, types.String(), `\"`), + Entry("raw windows path", `r"C:\Users\path"`, types.String(), `C:\Users\path`), + Entry("empty multi", "``", types.String(), ""), + Entry("simple multi", "`hello`", types.String(), "hello"), + Entry("multi with real newline", "`line1\nline2`", types.String(), "line1\nline2"), + Entry("multi three-line literal", "`a\nb\nc`", types.String(), "a\nb\nc"), + Entry("multi indentation preserved", "`a\n b`", types.String(), "a\n b"), + Entry("multi tab char preserved", "`a\tb`", types.String(), "a\tb"), + Entry("multi unicode", "`Β°C`", types.String(), "Β°C"), + Entry("multi processes escape sequences", "`a\\nb`", types.String(), "a\nb"), + Entry("raw multi verbatim escapes", "r`a\\nb`", types.String(), `a\nb`), + Entry("raw multi with real newline", "r`line1\nline2`", types.String(), "line1\nline2"), + Entry("no target type infers string from raw", `r"hi"`, types.Type{}, "hi"), + Entry("format single preserves placeholders in body", `f"hi {x}"`, types.String(), "hi {x}"), + Entry("format single processes standard escapes", `f"a\nb {x}"`, types.String(), "a\nb {x}"), + Entry("format multi preserves placeholders in body", "f`v={x}\nt={t}`", types.String(), "v={x}\nt={t}"), + Entry("format multi processes escapes outside placeholders", "f`a\\tb {x}`", types.String(), "a\tb {x}"), + Entry("rf preserves placeholder and backslash verbatim", `rf"C:\path\{x}"`, types.String(), `C:\path\{x}`), + Entry("fr (order-flipped) behaves identically to rf", `fr"hi {x}"`, types.String(), `hi {x}`), + Entry("rf multi preserves placeholder, real newline, and backslash", "rf`v={x}\nraw=\\n`", types.String(), "v={x}\nraw=\\n"), ) - DescribeTable("ParseRawString errors", + DescribeTable("ParseString errors", func(input string, target types.Type, errSubstring string) { - Expect(literal.ParseRawString(input, target)). + Expect(literal.ParseString(input, target)). Error().To(MatchError(ContainSubstring(errSubstring))) }, - Entry("non-string target type", "`hi`", types.I32(), "cannot assign string to"), - Entry("missing leading backtick", "hi`", types.String(), "invalid raw string literal"), - Entry("missing trailing backtick", "`hi", types.String(), "invalid raw string literal"), - Entry("single backtick too short", "`", types.String(), "invalid raw string literal"), - Entry("empty text too short", "", types.String(), "invalid raw string literal"), + Entry("non-string target type", `r"hi"`, types.I32(), "cannot assign string to"), + Entry("missing leading quote", `hi"`, types.String(), "invalid string literal"), + Entry("missing trailing quote", `"hi`, types.String(), "invalid string literal"), + Entry("single quote too short", `"`, types.String(), "invalid string literal"), + Entry("empty text too short", "", types.String(), "invalid string literal"), ) Describe("Parse AST routing", func() { - It("Should route backtick literal to ParseRawString", func() { - lit := getLiteral("`hello`") + It("Should route raw string literal through ParseString", func() { + lit := getLiteral(`r"hello"`) parsed := MustSucceed(literal.Parse(lit, types.String())) Expect(parsed.Value).To(Equal("hello")) Expect(parsed.Type).To(Equal(types.String())) }) - It("Should preserve real newlines in multi-line backtick literal", func() { + It("Should preserve real newlines in multi-line literal", func() { lit := getLiteral("`a\nb`") parsed := MustSucceed(literal.Parse(lit, types.String())) Expect(parsed.Value).To(Equal("a\nb")) }) It("Should infer string type when no target type specified", func() { - lit := getLiteral("`hi`") + lit := getLiteral(`r"hi"`) parsed := MustSucceed(literal.Parse(lit, types.Type{})) Expect(parsed.Value).To(Equal("hi")) Expect(parsed.Type).To(Equal(types.String())) }) - It("Should unescape backticks in raw string through Parse", func() { - lit := getLiteral("`say \\`hi\\``") - parsed := MustSucceed(literal.Parse(lit, types.String())) - Expect(parsed.Value).To(Equal("say `hi`")) - }) - It("Should still route regular string literals to ParseString", func() { lit := getLiteral(`"hello\n"`) parsed := MustSucceed(literal.Parse(lit, types.String())) diff --git a/arc/go/lsp/context.go b/arc/go/lsp/context.go index 38a3ffa5f1..967d50276b 100644 --- a/arc/go/lsp/context.go +++ b/arc/go/lsp/context.go @@ -345,7 +345,8 @@ func isStatementStartContext(tokens []antlr.Token, lastToken antlr.Token, pos pr parser.ArcLexerIDENTIFIER, parser.ArcLexerINTEGER_LITERAL, parser.ArcLexerFLOAT_LITERAL, - parser.ArcLexerSTR_LITERAL: + parser.ArcLexerSTR_LITERAL, + parser.ArcLexerSTR_LITERAL_MULTI: return true } } diff --git a/arc/go/lsp/semantic.go b/arc/go/lsp/semantic.go index 889a0764ed..ff6d6fb981 100644 --- a/arc/go/lsp/semantic.go +++ b/arc/go/lsp/semantic.go @@ -14,8 +14,10 @@ import ( "github.com/antlr4-go/antlr/v4" "github.com/synnaxlabs/arc/ir" + "github.com/synnaxlabs/arc/literal" "github.com/synnaxlabs/arc/parser" "github.com/synnaxlabs/arc/symbol" + "github.com/synnaxlabs/x/diagnostics" "github.com/synnaxlabs/x/lsp" "github.com/synnaxlabs/x/lsp/protocol" ) @@ -42,7 +44,7 @@ const ( SemanticTokenTypeInput SemanticTokenTypeOutput SemanticTokenTypeUnit - SemanticTokenTypeStringRaw + SemanticTokenTypeStringPlaceholder ) var semanticTokenTypes = []string{ @@ -67,7 +69,7 @@ var semanticTokenTypes = []string{ "input", "output", "unit", - "stringRaw", + "stringPlaceholder", } func (s *Server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { @@ -87,6 +89,11 @@ func extractSemanticTokens(ctx context.Context, content string, docIR ir.IR) []u if t.GetTokenType() == antlr.TokenEOF { continue } + if t.GetTokenType() == parser.ArcLexerSTR_LITERAL || + t.GetTokenType() == parser.ArcLexerSTR_LITERAL_MULTI { + tokens = append(tokens, expandStringToken(ctx, t, docIR)...) + continue + } var prevType, nextType int if i > 0 { prevType = allTokens[i-1].GetTokenType() @@ -105,14 +112,25 @@ func extractSemanticTokens(ctx context.Context, content string, docIR ir.IR) []u // appendTokenPerLine emits one LSP semantic token per source line covered by t. // Monaco does not render a single semantic token whose length crosses a newline, -// so multi-line ANTLR tokens (e.g. STR_LITERAL_RAW spanning several lines) must -// be split into per-line entries to receive consistent coloring across the whole -// span. For single-line tokens this collapses to one append, matching the prior -// behavior. +// so multi-line ANTLR tokens (e.g. STR_LITERAL_MULTI spanning several lines) +// must be split into per-line entries to receive consistent coloring across the +// whole span. For single-line tokens this collapses to one append, matching the +// prior behavior. func appendTokenPerLine(tokens []lsp.Token, t antlr.Token, tokenType uint32) []lsp.Token { - text := t.GetText() - line := uint32(t.GetLine() - 1) - startChar := uint32(t.GetColumn()) + return appendTextTokenPerLine( + tokens, + t.GetText(), + uint32(t.GetLine()-1), + uint32(t.GetColumn()), + tokenType, + ) +} + +func appendTextTokenPerLine( + tokens []lsp.Token, + text string, + line, startChar, tokenType uint32, +) []lsp.Token { lineStart := 0 for i := 0; i < len(text); i++ { if text[i] != '\n' { @@ -142,6 +160,17 @@ func appendTokenPerLine(tokens []lsp.Token, t antlr.Token, tokenType uint32) []l } func classifyToken(ctx context.Context, t antlr.Token, prevTokenType, nextTokenType int, docIR ir.IR) *uint32 { + return classifyTokenAt(ctx, t, prevTokenType, nextTokenType, docIR, t.GetLine(), t.GetColumn()) +} + +// Variant with explicit (line1, col0) for tokens lexed out of a sub-string. +func classifyTokenAt( + ctx context.Context, + t antlr.Token, + prevTokenType, nextTokenType int, + docIR ir.IR, + line1, col0 int, +) *uint32 { antlrType := t.GetTokenType() // IDENTIFIER after DOT is the member part of a qualified name // (e.g., "set" in "authority.set"). Color it as a function. @@ -150,7 +179,7 @@ func classifyToken(ctx context.Context, t antlr.Token, prevTokenType, nextTokenT return &tokenType } if antlrType == parser.ArcLexerIDENTIFIER && docIR.Symbols != nil { - return classifyIdentifier(ctx, t, docIR.Symbols) + return classifyIdentifierAt(ctx, t.GetText(), line1, col0, docIR.Symbols) } // AUTHORITY followed by DOT is a module prefix (authority.set), not the // authority keyword. Color it as a namespace/variable instead of a keyword. @@ -161,12 +190,8 @@ func classifyToken(ctx context.Context, t antlr.Token, prevTokenType, nextTokenT return mapLexerTokenType(antlrType) } -func classifyIdentifier(ctx context.Context, t antlr.Token, rootScope *symbol.Scope) *uint32 { - var ( - name = t.GetText() - pos = position{Line: t.GetLine(), Col: t.GetColumn()} - scope = findScopeAtInternalPosition(rootScope, pos) - ) +func classifyIdentifierAt(ctx context.Context, name string, line1, col0 int, rootScope *symbol.Scope) *uint32 { + scope := findScopeAtInternalPosition(rootScope, position{Line: line1, Col: col0}) sym, err := scope.Resolve(ctx, name) if err != nil || sym == nil { return nil @@ -233,10 +258,8 @@ func mapLexerTokenType(antlrType int) *uint32 { parser.ArcLexerEQ, parser.ArcLexerNEQ, parser.ArcLexerLT, parser.ArcLexerGT, parser.ArcLexerLEQ, parser.ArcLexerGEQ: tokenType = SemanticTokenTypeOperator - case parser.ArcLexerSTR_LITERAL: + case parser.ArcLexerSTR_LITERAL, parser.ArcLexerSTR_LITERAL_MULTI: tokenType = SemanticTokenTypeString - case parser.ArcLexerSTR_LITERAL_RAW: - tokenType = SemanticTokenTypeStringRaw case parser.ArcLexerINTEGER_LITERAL, parser.ArcLexerFLOAT_LITERAL: tokenType = SemanticTokenTypeNumber case parser.ArcLexerSINGLE_LINE_COMMENT, parser.ArcLexerMULTI_LINE_COMMENT: @@ -248,3 +271,99 @@ func mapLexerTokenType(antlrType int) *uint32 { } return &tokenType } + +// expandStringToken emits LSP semantic tokens for a STR_LITERAL or +// STR_LITERAL_MULTI. Format strings (f/rf prefix) with placeholders have their +// {...} segments expanded with classified inner expression tokens; everything +// else collapses to a single string-colored token. +func expandStringToken(ctx context.Context, t antlr.Token, docIR ir.IR) []lsp.Token { + text := t.GetText() + body, flags, ok := literal.StripQuotes(text) + prefixLen := 0 + for prefixLen < 2 && prefixLen < len(text) && (text[prefixLen] == 'r' || text[prefixLen] == 'f') { + prefixLen++ + } + fallback := func() []lsp.Token { + var out []lsp.Token + line, col := uint32(t.GetLine()-1), uint32(t.GetColumn()) + if prefixLen > 0 { + out = appendTextTokenPerLine(out, text[:prefixLen], line, col, SemanticTokenTypeFunction) + col += uint32(prefixLen) + } + out = appendTextTokenPerLine(out, text[prefixLen:], line, col, SemanticTokenTypeString) + return out + } + if !ok || !flags.Format { + return fallback() + } + segs, err := literal.FmtStrParse(body) + if err != nil { + return fallback() + } + if !literal.FmtStrHasPlaceholder(segs) { + return fallback() + } + const delimLen = 1 + bodyOff := prefixLen + delimLen + cursor := diagnostics.Position{Line: t.GetLine() - 1, Col: t.GetColumn()} + prevOff := 0 + posOf := func(off int) (uint32, uint32) { + cursor = cursor.Advance(text[prevOff:], off-prevOff) + prevOff = off + return uint32(cursor.Line), uint32(cursor.Col) + } + var tokens []lsp.Token + emit := func(a, b int, tt uint32) { + if a >= b { + return + } + line, col := posOf(a) + tokens = appendTextTokenPerLine(tokens, text[a:b], line, col, tt) + } + emitInner := func(a, b int) { + inner := tokenizeContent(text[a:b]) + baseLine, baseCol := posOf(a) + for i, it := range inner { + if it.GetTokenType() == antlr.TokenEOF { + continue + } + var prev, next int + if i > 0 { + prev = inner[i-1].GetTokenType() + } + if i+1 < len(inner) { + next = inner[i+1].GetTokenType() + } + relLine, relCol := uint32(it.GetLine()-1), uint32(it.GetColumn()) + absLine, absCol := baseLine+relLine, relCol + if relLine == 0 { + absCol = baseCol + relCol + } + tt := classifyTokenAt(ctx, it, prev, next, docIR, int(absLine)+1, int(absCol)) + if tt == nil { + continue + } + tokens = appendTextTokenPerLine(tokens, it.GetText(), absLine, absCol, *tt) + } + } + emit(0, prefixLen, SemanticTokenTypeFunction) + emit(prefixLen, bodyOff, SemanticTokenTypeString) + for _, seg := range segs { + if !seg.IsPlaceholder { + emit(seg.Start+bodyOff, seg.End+bodyOff, SemanticTokenTypeString) + continue + } + emit(seg.Start+bodyOff, seg.Start+bodyOff+1, SemanticTokenTypeStringPlaceholder) + exprEnd := seg.End - 1 + if seg.SpecOffset >= 0 { + exprEnd = seg.SpecOffset + } + emitInner(seg.Start+bodyOff+1, exprEnd+bodyOff) + if seg.SpecOffset >= 0 { + emit(seg.SpecOffset+bodyOff, seg.End-1+bodyOff, SemanticTokenTypeStringPlaceholder) + } + emit(seg.End-1+bodyOff, seg.End+bodyOff, SemanticTokenTypeStringPlaceholder) + } + emit(len(text)-delimLen, len(text), SemanticTokenTypeString) + return tokens +} diff --git a/arc/go/lsp/semantic_test.go b/arc/go/lsp/semantic_test.go index 6ae8a7a016..24b4189363 100644 --- a/arc/go/lsp/semantic_test.go +++ b/arc/go/lsp/semantic_test.go @@ -14,17 +14,23 @@ import ( . "github.com/onsi/gomega" "github.com/synnaxlabs/arc/lsp" . "github.com/synnaxlabs/arc/lsp/testutil" + "github.com/synnaxlabs/arc/symbol" + "github.com/synnaxlabs/arc/types" "github.com/synnaxlabs/x/lsp/protocol" . "github.com/synnaxlabs/x/lsp/testutil" . "github.com/synnaxlabs/x/testutil" ) // Token type ids must mirror the iota constants in arc/go/lsp/semantic.go. -// Tests in this file pin both the legend ordering and the token-type routing -// for STR_LITERAL vs STR_LITERAL_RAW to those ids. +// Tests in this file pin the legend ordering and the token-type routing +// to those ids. const ( - tokenTypeString = uint32(4) - tokenTypeStringRaw = uint32(21) + tokenTypeOperator = uint32(2) + tokenTypeString = uint32(4) + tokenTypeNumber = uint32(5) + tokenTypeFunction = uint32(7) + tokenTypeChannel = uint32(9) + tokenTypeStringPlaceholder = uint32(21) ) // decodeSemanticTokens turns the LSP delta-encoded uint32 stream from @@ -85,8 +91,8 @@ var _ = Describe("Semantic Tokens", func() { Describe("appendTokenPerLine β€” raw string spans", func() { It("emits one token for a single-line raw literal", func(ctx SpecContext) { - OpenArcDocument(server, ctx, uri, "x := `hello`") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) + OpenArcDocument(server, ctx, uri, "x := \"hello\"") + tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeString) Expect(tokens).To(HaveLen(1)) Expect(tokens[0].Line).To(Equal(uint32(0))) Expect(tokens[0].StartChar).To(Equal(uint32(5))) @@ -94,17 +100,17 @@ var _ = Describe("Semantic Tokens", func() { }) It("emits one token for an empty raw literal", func(ctx SpecContext) { - OpenArcDocument(server, ctx, uri, "x := ``") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) + OpenArcDocument(server, ctx, uri, "x := \"\"") + tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeString) Expect(tokens).To(HaveLen(1)) Expect(tokens[0].Line).To(Equal(uint32(0))) Expect(tokens[0].StartChar).To(Equal(uint32(5))) Expect(tokens[0].Length).To(Equal(uint32(2))) }) - It("splits a raw literal with one mid-newline into two tokens", func(ctx SpecContext) { + It("splits a multi-line literal with one mid-newline into two tokens", func(ctx SpecContext) { OpenArcDocument(server, ctx, uri, "x := `a\nb`") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) + tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeString) Expect(tokens).To(HaveLen(2)) Expect(tokens[0].Line).To(Equal(uint32(0))) Expect(tokens[0].StartChar).To(Equal(uint32(5))) @@ -114,9 +120,9 @@ var _ = Describe("Semantic Tokens", func() { Expect(tokens[1].Length).To(Equal(uint32(2))) }) - It("splits a three-line raw literal into three tokens", func(ctx SpecContext) { + It("splits a three-line multi-line literal into three tokens", func(ctx SpecContext) { OpenArcDocument(server, ctx, uri, "x := `a\nb\nc`") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) + tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeString) Expect(tokens).To(HaveLen(3)) Expect(tokens[0].Line).To(Equal(uint32(0))) Expect(tokens[0].StartChar).To(Equal(uint32(5))) @@ -131,7 +137,7 @@ var _ = Describe("Semantic Tokens", func() { It("emits a final token for a closing backtick on its own line", func(ctx SpecContext) { OpenArcDocument(server, ctx, uri, "x := `abc\n`") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) + tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeString) Expect(tokens).To(HaveLen(2)) Expect(tokens[0].Line).To(Equal(uint32(0))) Expect(tokens[0].StartChar).To(Equal(uint32(5))) @@ -141,18 +147,9 @@ var _ = Describe("Semantic Tokens", func() { Expect(tokens[1].Length).To(Equal(uint32(1))) }) - It("emits one token for a raw literal with escaped backticks", func(ctx SpecContext) { - OpenArcDocument(server, ctx, uri, "x := `say \\`hi\\``") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) - Expect(tokens).To(HaveLen(1)) - Expect(tokens[0].Line).To(Equal(uint32(0))) - Expect(tokens[0].StartChar).To(Equal(uint32(5))) - Expect(tokens[0].Length).To(Equal(uint32(12))) - }) - - It("skips empty lines in a raw literal with consecutive newlines", func(ctx SpecContext) { + It("skips empty lines in a multi-line literal with consecutive newlines", func(ctx SpecContext) { OpenArcDocument(server, ctx, uri, "x := `a\n\nb`") - tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeStringRaw) + tokens := filterByType(decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data), tokenTypeString) Expect(tokens).To(HaveLen(2)) Expect(tokens[0].Line).To(Equal(uint32(0))) Expect(tokens[0].StartChar).To(Equal(uint32(5))) @@ -164,32 +161,194 @@ var _ = Describe("Semantic Tokens", func() { }) Describe("Token Type Routing", func() { - It("routes backtick literals to the stringRaw token type", func(ctx SpecContext) { - OpenArcDocument(server, ctx, uri, "x := `hi`") + It("routes single-quoted literals to the string token type", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := "hi"`) tokens := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) - raw := filterByType(tokens, tokenTypeStringRaw) - Expect(raw).To(HaveLen(1)) - Expect(raw[0].Length).To(Equal(uint32(4))) + str := filterByType(tokens, tokenTypeString) + Expect(str).To(HaveLen(1)) + Expect(str[0].Length).To(Equal(uint32(4))) }) - It("routes regular double-quoted strings to the string token type", func(ctx SpecContext) { - OpenArcDocument(server, ctx, uri, `x := "hi"`) + It("routes raw-prefixed literals to the string token type", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := r"hi"`) tokens := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) str := filterByType(tokens, tokenTypeString) Expect(str).To(HaveLen(1)) Expect(str[0].Length).To(Equal(uint32(4))) - Expect(filterByType(tokens, tokenTypeStringRaw)).To(BeEmpty()) }) - It("does not emit a stringRaw token when no backtick literal is present", func(ctx SpecContext) { + It("does not emit a string token when no string literal is present", func(ctx SpecContext) { OpenArcDocument(server, ctx, uri, "x := 42") tokens := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) - Expect(filterByType(tokens, tokenTypeStringRaw)).To(BeEmpty()) + Expect(filterByType(tokens, tokenTypeString)).To(BeEmpty()) + }) + + It("routes raw triple-quoted literals to the string token type across newlines", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, "x := r\"\"\"a\nb\"\"\"") + tokens := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + str := filterByType(tokens, tokenTypeString) + Expect(str).To(HaveLen(2)) + Expect(str[0].Line).To(Equal(uint32(0))) + Expect(str[1].Line).To(Equal(uint32(1))) + }) + }) + + Describe("Format-string placeholders", func() { + It("splits f\"val: {42}\" into prefix, string segments, placeholder braces, and a number", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := f"val: {42}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + var inLit []decodedToken + for _, t := range all { + if t.Line == 0 && t.StartChar >= 5 { + inLit = append(inLit, t) + } + } + Expect(inLit).To(HaveLen(7)) + Expect(inLit[0]).To(Equal(decodedToken{Line: 0, StartChar: 5, Length: 1, TokenType: tokenTypeFunction})) + Expect(inLit[1]).To(Equal(decodedToken{Line: 0, StartChar: 6, Length: 1, TokenType: tokenTypeString})) + Expect(inLit[2]).To(Equal(decodedToken{Line: 0, StartChar: 7, Length: 5, TokenType: tokenTypeString})) + Expect(inLit[3]).To(Equal(decodedToken{Line: 0, StartChar: 12, Length: 1, TokenType: tokenTypeStringPlaceholder})) + Expect(inLit[4]).To(Equal(decodedToken{Line: 0, StartChar: 13, Length: 2, TokenType: tokenTypeNumber})) + Expect(inLit[5]).To(Equal(decodedToken{Line: 0, StartChar: 15, Length: 1, TokenType: tokenTypeStringPlaceholder})) + Expect(inLit[6]).To(Equal(decodedToken{Line: 0, StartChar: 16, Length: 1, TokenType: tokenTypeString})) + }) + + It("classifies a placeholder identifier through the global resolver", func(ctx SpecContext) { + globalResolver := symbol.MapResolver{ + "sensorData": symbol.Symbol{ + Name: "sensorData", + Type: types.Chan(types.F64()), + Kind: symbol.KindChannel, + }, + } + server, uri = SetupTestServer(lsp.Config{GlobalResolver: globalResolver}) + OpenArcDocument(server, ctx, uri, `x := f"v: {sensorData}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + ch := filterByType(all, tokenTypeChannel) + Expect(ch).To(HaveLen(1)) + Expect(ch[0]).To(Equal(decodedToken{Line: 0, StartChar: 11, Length: 10, TokenType: tokenTypeChannel})) + }) + + It("treats {{ as a literal-brace escape and leaves bare } literal", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := f"a {{ b }} c"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeStringPlaceholder)).To(BeEmpty()) + str := filterByType(all, tokenTypeString) + Expect(str).ToNot(BeEmpty()) + }) + + It("recognizes a real placeholder while ignoring surrounding doubled braces", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := f"{{ {42} }}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeStringPlaceholder)).To(HaveLen(2)) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(1)) + }) + + It("falls back to prefix + string tokens on a malformed placeholder", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := f"unterminated {x"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + str := filterByType(all, tokenTypeString) + Expect(str).To(HaveLen(1)) + Expect(str[0]).To(Equal(decodedToken{Line: 0, StartChar: 6, Length: 17, TokenType: tokenTypeString})) + fn := filterByType(all, tokenTypeFunction) + Expect(fn).To(HaveLen(1)) + Expect(fn[0]).To(Equal(decodedToken{Line: 0, StartChar: 5, Length: 1, TokenType: tokenTypeFunction})) + for _, op := range filterByType(all, tokenTypeOperator) { + Expect(op.StartChar < 5).To(BeTrue()) + } + }) + + It("emits a placeholder span for a numeric format spec after the expression", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := f"v={42:05d}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + ph := filterByType(all, tokenTypeStringPlaceholder) + Expect(ph).To(HaveLen(3)) + Expect(ph[0]).To(Equal(decodedToken{Line: 0, StartChar: 9, Length: 1, TokenType: tokenTypeStringPlaceholder})) + Expect(ph[1]).To(Equal(decodedToken{Line: 0, StartChar: 12, Length: 4, TokenType: tokenTypeStringPlaceholder})) + Expect(ph[2]).To(Equal(decodedToken{Line: 0, StartChar: 16, Length: 1, TokenType: tokenTypeStringPlaceholder})) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(1)) + }) + + It("classifies multi-token placeholder expressions with prev/next context", func(ctx SpecContext) { + globalResolver := symbol.MapResolver{ + "sensor": symbol.Symbol{ + Name: "sensor", + Type: types.Chan(types.F64()), + Kind: symbol.KindChannel, + }, + } + server, uri = SetupTestServer(lsp.Config{GlobalResolver: globalResolver}) + OpenArcDocument(server, ctx, uri, `x := f"v={sensor + 1}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeChannel)).To(HaveLen(1)) + plus := filterByType(all, tokenTypeOperator) + Expect(plus).ToNot(BeEmpty()) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(1)) + }) + + It("skips inner placeholder tokens that classify to nil (parens)", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := f"v={(42)}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(1)) + Expect(filterByType(all, tokenTypeStringPlaceholder)).To(HaveLen(2)) + }) + + It("classifies placeholders across newlines in a multi-line format string", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, "x := f`a={1}\nb={2}`") + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(2)) + Expect(filterByType(all, tokenTypeStringPlaceholder)).To(HaveLen(4)) + }) + + It("classifies placeholders inside an rf-prefixed format string", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := rf"v={42}"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(1)) + Expect(filterByType(all, tokenTypeStringPlaceholder)).To(HaveLen(2)) + }) + + It("classifies placeholders inside an rf-prefixed multi-line format string", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, "x := rf`a={1}\nb={2}`") + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeNumber)).To(HaveLen(2)) + Expect(filterByType(all, tokenTypeStringPlaceholder)).To(HaveLen(4)) + }) + + DescribeTable("emits the r/f/rf/fr prefix as a function-typed token", + func(ctx SpecContext, source string, prefixLen uint32) { + OpenArcDocument(server, ctx, uri, source) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + fn := filterByType(all, tokenTypeFunction) + Expect(fn).To(HaveLen(1)) + Expect(fn[0]).To(Equal(decodedToken{Line: 0, StartChar: 5, Length: prefixLen, TokenType: tokenTypeFunction})) + }, + Entry("f-prefixed single-quoted", `x := f"hi {x}"`, uint32(1)), + Entry("r-prefixed single-quoted", `x := r"path"`, uint32(1)), + Entry("rf-prefixed single-quoted", `x := rf"hi {x}"`, uint32(2)), + Entry("fr-prefixed single-quoted", `x := fr"hi {x}"`, uint32(2)), + Entry("f-prefixed backtick", "x := f`hi`", uint32(1)), + Entry("r-prefixed backtick", "x := r`hi`", uint32(1)), + Entry("rf-prefixed backtick", "x := rf`hi {x}`", uint32(2)), + ) + + It("does not emit a function token for an unprefixed string", func(ctx SpecContext) { + OpenArcDocument(server, ctx, uri, `x := "plain"`) + all := decodeSemanticTokens(SemanticTokens(server, ctx, uri).Data) + Expect(filterByType(all, tokenTypeFunction)).To(BeEmpty()) + }) + }) + + Describe("SemanticTokensFull", func() { + It("returns an empty token stream for an unknown document URI", func(ctx SpecContext) { + result := MustSucceed(server.SemanticTokensFull(ctx, &protocol.SemanticTokensParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: "file:///not-open.arc"}, + })) + Expect(result.Data).To(BeEmpty()) }) }) Describe("Legend", func() { - It("places stringRaw last in the semantic token types legend", func(ctx SpecContext) { + It("registers stringPlaceholder at the end of the semantic token types legend", func(ctx SpecContext) { result := MustSucceed(server.Initialize(ctx, &protocol.InitializeParams{ ClientInfo: &protocol.ClientInfo{Name: "test"}, })) @@ -198,9 +357,9 @@ var _ = Describe("Semantic Tokens", func() { legend, ok := provider["legend"].(protocol.SemanticTokensLegend) Expect(ok).To(BeTrue()) Expect(legend.TokenTypes).ToNot(BeEmpty()) - last := legend.TokenTypes[len(legend.TokenTypes)-1] - Expect(string(last)).To(Equal("stringRaw")) - Expect(uint32(len(legend.TokenTypes) - 1)).To(Equal(tokenTypeStringRaw)) + n := len(legend.TokenTypes) + Expect(string(legend.TokenTypes[n-1])).To(Equal("stringPlaceholder")) + Expect(uint32(n - 1)).To(Equal(tokenTypeStringPlaceholder)) }) }) }) diff --git a/arc/go/parser/ArcLexer.g4 b/arc/go/parser/ArcLexer.g4 index 86358a5499..f5fc127c5f 100644 --- a/arc/go/parser/ArcLexer.g4 +++ b/arc/go/parser/ArcLexer.g4 @@ -111,20 +111,23 @@ FLOAT_LITERAL | '.' DIGITS ; -// String literal -STR_LITERAL - : '"' (~["\\\r\n] | ESCAPE_SEQUENCE)* '"' +// Multi-line string literal. Backtick-delimited; may span newlines. An optional +// prefix selects raw and/or format semantics. The lexer is permissive: any +// '\\' followed by any character is accepted, and the literal package +// interprets escapes (or skips them for raw strings). +STR_LITERAL_MULTI + : STR_PREFIX? '`' (~[`\\] | '\\' .)* '`' ; -// Raw multi-line string literal. Supports \` for a literal backtick; all other -// content is verbatim. -STR_LITERAL_RAW - : '`' ('\\`' | ~[`])* '`' +// Single-line string literal. An optional prefix selects raw and/or format +// semantics. The lexer accepts any '\\' followed by any character; the literal +// package validates and interprets escapes. +STR_LITERAL + : STR_PREFIX? '"' (~["\\\r\n] | '\\' .)* '"' ; -fragment ESCAPE_SEQUENCE - : '\\' [btnfr"\\] - | '\\u' [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] +fragment STR_PREFIX + : 'r' | 'f' | 'rf' | 'fr' ; // ============================================================================= diff --git a/arc/go/parser/ArcLexer.interp b/arc/go/parser/ArcLexer.interp index 8bb7bcdb06..8bd31e808a 100644 --- a/arc/go/parser/ArcLexer.interp +++ b/arc/go/parser/ArcLexer.interp @@ -129,8 +129,8 @@ COLON DOT INTEGER_LITERAL FLOAT_LITERAL +STR_LITERAL_MULTI STR_LITERAL -STR_LITERAL_RAW IDENTIFIER SINGLE_LINE_COMMENT MULTI_LINE_COMMENT @@ -199,9 +199,9 @@ DIGITS DIGIT INTEGER_LITERAL FLOAT_LITERAL +STR_LITERAL_MULTI STR_LITERAL -STR_LITERAL_RAW -ESCAPE_SEQUENCE +STR_PREFIX IDENTIFIER SINGLE_LINE_COMMENT MULTI_LINE_COMMENT @@ -215,4 +215,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 66, 437, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 1, 37, 1, 38, 1, 38, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 50, 1, 50, 1, 51, 1, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 4, 58, 350, 8, 58, 11, 58, 12, 58, 351, 1, 59, 1, 59, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 3, 61, 361, 8, 61, 1, 61, 1, 61, 3, 61, 365, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 370, 8, 62, 10, 62, 12, 62, 373, 9, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 5, 63, 381, 8, 63, 10, 63, 12, 63, 384, 9, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 397, 8, 64, 1, 65, 1, 65, 5, 65, 401, 8, 65, 10, 65, 12, 65, 404, 9, 65, 1, 66, 1, 66, 1, 66, 1, 66, 5, 66, 410, 8, 66, 10, 66, 12, 66, 413, 9, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 421, 8, 67, 10, 67, 12, 67, 424, 9, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 4, 68, 432, 8, 68, 11, 68, 12, 68, 433, 1, 68, 1, 68, 1, 422, 0, 69, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, 65, 33, 67, 34, 69, 35, 71, 36, 73, 37, 75, 38, 77, 39, 79, 40, 81, 41, 83, 42, 85, 43, 87, 44, 89, 45, 91, 46, 93, 47, 95, 48, 97, 49, 99, 50, 101, 51, 103, 52, 105, 53, 107, 54, 109, 55, 111, 56, 113, 57, 115, 58, 117, 0, 119, 0, 121, 59, 123, 60, 125, 61, 127, 62, 129, 0, 131, 63, 133, 64, 135, 65, 137, 66, 1, 0, 9, 1, 0, 48, 57, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 1, 0, 96, 96, 7, 0, 34, 34, 92, 92, 98, 98, 102, 102, 110, 110, 114, 114, 116, 116, 3, 0, 48, 57, 65, 70, 97, 102, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 445, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 89, 1, 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 95, 1, 0, 0, 0, 0, 97, 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, 0, 0, 0, 0, 103, 1, 0, 0, 0, 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, 109, 1, 0, 0, 0, 0, 111, 1, 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 115, 1, 0, 0, 0, 0, 121, 1, 0, 0, 0, 0, 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 131, 1, 0, 0, 0, 0, 133, 1, 0, 0, 0, 0, 135, 1, 0, 0, 0, 0, 137, 1, 0, 0, 0, 1, 139, 1, 0, 0, 0, 3, 144, 1, 0, 0, 0, 5, 147, 1, 0, 0, 0, 7, 152, 1, 0, 0, 0, 9, 159, 1, 0, 0, 0, 11, 163, 1, 0, 0, 0, 13, 169, 1, 0, 0, 0, 15, 178, 1, 0, 0, 0, 17, 187, 1, 0, 0, 0, 19, 193, 1, 0, 0, 0, 21, 198, 1, 0, 0, 0, 23, 203, 1, 0, 0, 0, 25, 213, 1, 0, 0, 0, 27, 216, 1, 0, 0, 0, 29, 220, 1, 0, 0, 0, 31, 224, 1, 0, 0, 0, 33, 228, 1, 0, 0, 0, 35, 231, 1, 0, 0, 0, 37, 235, 1, 0, 0, 0, 39, 239, 1, 0, 0, 0, 41, 243, 1, 0, 0, 0, 43, 247, 1, 0, 0, 0, 45, 251, 1, 0, 0, 0, 47, 255, 1, 0, 0, 0, 49, 262, 1, 0, 0, 0, 51, 265, 1, 0, 0, 0, 53, 268, 1, 0, 0, 0, 55, 271, 1, 0, 0, 0, 57, 274, 1, 0, 0, 0, 59, 276, 1, 0, 0, 0, 61, 279, 1, 0, 0, 0, 63, 282, 1, 0, 0, 0, 65, 285, 1, 0, 0, 0, 67, 288, 1, 0, 0, 0, 69, 291, 1, 0, 0, 0, 71, 293, 1, 0, 0, 0, 73, 295, 1, 0, 0, 0, 75, 297, 1, 0, 0, 0, 77, 299, 1, 0, 0, 0, 79, 301, 1, 0, 0, 0, 81, 303, 1, 0, 0, 0, 83, 306, 1, 0, 0, 0, 85, 309, 1, 0, 0, 0, 87, 311, 1, 0, 0, 0, 89, 313, 1, 0, 0, 0, 91, 316, 1, 0, 0, 0, 93, 319, 1, 0, 0, 0, 95, 323, 1, 0, 0, 0, 97, 326, 1, 0, 0, 0, 99, 330, 1, 0, 0, 0, 101, 332, 1, 0, 0, 0, 103, 334, 1, 0, 0, 0, 105, 336, 1, 0, 0, 0, 107, 338, 1, 0, 0, 0, 109, 340, 1, 0, 0, 0, 111, 342, 1, 0, 0, 0, 113, 344, 1, 0, 0, 0, 115, 346, 1, 0, 0, 0, 117, 349, 1, 0, 0, 0, 119, 353, 1, 0, 0, 0, 121, 355, 1, 0, 0, 0, 123, 364, 1, 0, 0, 0, 125, 366, 1, 0, 0, 0, 127, 376, 1, 0, 0, 0, 129, 396, 1, 0, 0, 0, 131, 398, 1, 0, 0, 0, 133, 405, 1, 0, 0, 0, 135, 416, 1, 0, 0, 0, 137, 431, 1, 0, 0, 0, 139, 140, 5, 102, 0, 0, 140, 141, 5, 117, 0, 0, 141, 142, 5, 110, 0, 0, 142, 143, 5, 99, 0, 0, 143, 2, 1, 0, 0, 0, 144, 145, 5, 105, 0, 0, 145, 146, 5, 102, 0, 0, 146, 4, 1, 0, 0, 0, 147, 148, 5, 101, 0, 0, 148, 149, 5, 108, 0, 0, 149, 150, 5, 115, 0, 0, 150, 151, 5, 101, 0, 0, 151, 6, 1, 0, 0, 0, 152, 153, 5, 114, 0, 0, 153, 154, 5, 101, 0, 0, 154, 155, 5, 116, 0, 0, 155, 156, 5, 117, 0, 0, 156, 157, 5, 114, 0, 0, 157, 158, 5, 110, 0, 0, 158, 8, 1, 0, 0, 0, 159, 160, 5, 102, 0, 0, 160, 161, 5, 111, 0, 0, 161, 162, 5, 114, 0, 0, 162, 10, 1, 0, 0, 0, 163, 164, 5, 98, 0, 0, 164, 165, 5, 114, 0, 0, 165, 166, 5, 101, 0, 0, 166, 167, 5, 97, 0, 0, 167, 168, 5, 107, 0, 0, 168, 12, 1, 0, 0, 0, 169, 170, 5, 99, 0, 0, 170, 171, 5, 111, 0, 0, 171, 172, 5, 110, 0, 0, 172, 173, 5, 116, 0, 0, 173, 174, 5, 105, 0, 0, 174, 175, 5, 110, 0, 0, 175, 176, 5, 117, 0, 0, 176, 177, 5, 101, 0, 0, 177, 14, 1, 0, 0, 0, 178, 179, 5, 115, 0, 0, 179, 180, 5, 101, 0, 0, 180, 181, 5, 113, 0, 0, 181, 182, 5, 117, 0, 0, 182, 183, 5, 101, 0, 0, 183, 184, 5, 110, 0, 0, 184, 185, 5, 99, 0, 0, 185, 186, 5, 101, 0, 0, 186, 16, 1, 0, 0, 0, 187, 188, 5, 115, 0, 0, 188, 189, 5, 116, 0, 0, 189, 190, 5, 97, 0, 0, 190, 191, 5, 103, 0, 0, 191, 192, 5, 101, 0, 0, 192, 18, 1, 0, 0, 0, 193, 194, 5, 110, 0, 0, 194, 195, 5, 101, 0, 0, 195, 196, 5, 120, 0, 0, 196, 197, 5, 116, 0, 0, 197, 20, 1, 0, 0, 0, 198, 199, 5, 99, 0, 0, 199, 200, 5, 104, 0, 0, 200, 201, 5, 97, 0, 0, 201, 202, 5, 110, 0, 0, 202, 22, 1, 0, 0, 0, 203, 204, 5, 97, 0, 0, 204, 205, 5, 117, 0, 0, 205, 206, 5, 116, 0, 0, 206, 207, 5, 104, 0, 0, 207, 208, 5, 111, 0, 0, 208, 209, 5, 114, 0, 0, 209, 210, 5, 105, 0, 0, 210, 211, 5, 116, 0, 0, 211, 212, 5, 121, 0, 0, 212, 24, 1, 0, 0, 0, 213, 214, 5, 105, 0, 0, 214, 215, 5, 56, 0, 0, 215, 26, 1, 0, 0, 0, 216, 217, 5, 105, 0, 0, 217, 218, 5, 49, 0, 0, 218, 219, 5, 54, 0, 0, 219, 28, 1, 0, 0, 0, 220, 221, 5, 105, 0, 0, 221, 222, 5, 51, 0, 0, 222, 223, 5, 50, 0, 0, 223, 30, 1, 0, 0, 0, 224, 225, 5, 105, 0, 0, 225, 226, 5, 54, 0, 0, 226, 227, 5, 52, 0, 0, 227, 32, 1, 0, 0, 0, 228, 229, 5, 117, 0, 0, 229, 230, 5, 56, 0, 0, 230, 34, 1, 0, 0, 0, 231, 232, 5, 117, 0, 0, 232, 233, 5, 49, 0, 0, 233, 234, 5, 54, 0, 0, 234, 36, 1, 0, 0, 0, 235, 236, 5, 117, 0, 0, 236, 237, 5, 51, 0, 0, 237, 238, 5, 50, 0, 0, 238, 38, 1, 0, 0, 0, 239, 240, 5, 117, 0, 0, 240, 241, 5, 54, 0, 0, 241, 242, 5, 52, 0, 0, 242, 40, 1, 0, 0, 0, 243, 244, 5, 102, 0, 0, 244, 245, 5, 51, 0, 0, 245, 246, 5, 50, 0, 0, 246, 42, 1, 0, 0, 0, 247, 248, 5, 102, 0, 0, 248, 249, 5, 54, 0, 0, 249, 250, 5, 52, 0, 0, 250, 44, 1, 0, 0, 0, 251, 252, 5, 115, 0, 0, 252, 253, 5, 116, 0, 0, 253, 254, 5, 114, 0, 0, 254, 46, 1, 0, 0, 0, 255, 256, 5, 115, 0, 0, 256, 257, 5, 101, 0, 0, 257, 258, 5, 114, 0, 0, 258, 259, 5, 105, 0, 0, 259, 260, 5, 101, 0, 0, 260, 261, 5, 115, 0, 0, 261, 48, 1, 0, 0, 0, 262, 263, 5, 45, 0, 0, 263, 264, 5, 62, 0, 0, 264, 50, 1, 0, 0, 0, 265, 266, 5, 58, 0, 0, 266, 267, 5, 61, 0, 0, 267, 52, 1, 0, 0, 0, 268, 269, 5, 36, 0, 0, 269, 270, 5, 61, 0, 0, 270, 54, 1, 0, 0, 0, 271, 272, 5, 61, 0, 0, 272, 273, 5, 62, 0, 0, 273, 56, 1, 0, 0, 0, 274, 275, 5, 61, 0, 0, 275, 58, 1, 0, 0, 0, 276, 277, 5, 43, 0, 0, 277, 278, 5, 61, 0, 0, 278, 60, 1, 0, 0, 0, 279, 280, 5, 45, 0, 0, 280, 281, 5, 61, 0, 0, 281, 62, 1, 0, 0, 0, 282, 283, 5, 42, 0, 0, 283, 284, 5, 61, 0, 0, 284, 64, 1, 0, 0, 0, 285, 286, 5, 47, 0, 0, 286, 287, 5, 61, 0, 0, 287, 66, 1, 0, 0, 0, 288, 289, 5, 37, 0, 0, 289, 290, 5, 61, 0, 0, 290, 68, 1, 0, 0, 0, 291, 292, 5, 43, 0, 0, 292, 70, 1, 0, 0, 0, 293, 294, 5, 45, 0, 0, 294, 72, 1, 0, 0, 0, 295, 296, 5, 42, 0, 0, 296, 74, 1, 0, 0, 0, 297, 298, 5, 47, 0, 0, 298, 76, 1, 0, 0, 0, 299, 300, 5, 37, 0, 0, 300, 78, 1, 0, 0, 0, 301, 302, 5, 94, 0, 0, 302, 80, 1, 0, 0, 0, 303, 304, 5, 61, 0, 0, 304, 305, 5, 61, 0, 0, 305, 82, 1, 0, 0, 0, 306, 307, 5, 33, 0, 0, 307, 308, 5, 61, 0, 0, 308, 84, 1, 0, 0, 0, 309, 310, 5, 60, 0, 0, 310, 86, 1, 0, 0, 0, 311, 312, 5, 62, 0, 0, 312, 88, 1, 0, 0, 0, 313, 314, 5, 60, 0, 0, 314, 315, 5, 61, 0, 0, 315, 90, 1, 0, 0, 0, 316, 317, 5, 62, 0, 0, 317, 318, 5, 61, 0, 0, 318, 92, 1, 0, 0, 0, 319, 320, 5, 97, 0, 0, 320, 321, 5, 110, 0, 0, 321, 322, 5, 100, 0, 0, 322, 94, 1, 0, 0, 0, 323, 324, 5, 111, 0, 0, 324, 325, 5, 114, 0, 0, 325, 96, 1, 0, 0, 0, 326, 327, 5, 110, 0, 0, 327, 328, 5, 111, 0, 0, 328, 329, 5, 116, 0, 0, 329, 98, 1, 0, 0, 0, 330, 331, 5, 40, 0, 0, 331, 100, 1, 0, 0, 0, 332, 333, 5, 41, 0, 0, 333, 102, 1, 0, 0, 0, 334, 335, 5, 123, 0, 0, 335, 104, 1, 0, 0, 0, 336, 337, 5, 125, 0, 0, 337, 106, 1, 0, 0, 0, 338, 339, 5, 91, 0, 0, 339, 108, 1, 0, 0, 0, 340, 341, 5, 93, 0, 0, 341, 110, 1, 0, 0, 0, 342, 343, 5, 44, 0, 0, 343, 112, 1, 0, 0, 0, 344, 345, 5, 58, 0, 0, 345, 114, 1, 0, 0, 0, 346, 347, 5, 46, 0, 0, 347, 116, 1, 0, 0, 0, 348, 350, 3, 119, 59, 0, 349, 348, 1, 0, 0, 0, 350, 351, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, 118, 1, 0, 0, 0, 353, 354, 7, 0, 0, 0, 354, 120, 1, 0, 0, 0, 355, 356, 3, 117, 58, 0, 356, 122, 1, 0, 0, 0, 357, 358, 3, 117, 58, 0, 358, 360, 5, 46, 0, 0, 359, 361, 3, 117, 58, 0, 360, 359, 1, 0, 0, 0, 360, 361, 1, 0, 0, 0, 361, 365, 1, 0, 0, 0, 362, 363, 5, 46, 0, 0, 363, 365, 3, 117, 58, 0, 364, 357, 1, 0, 0, 0, 364, 362, 1, 0, 0, 0, 365, 124, 1, 0, 0, 0, 366, 371, 5, 34, 0, 0, 367, 370, 8, 1, 0, 0, 368, 370, 3, 129, 64, 0, 369, 367, 1, 0, 0, 0, 369, 368, 1, 0, 0, 0, 370, 373, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 371, 372, 1, 0, 0, 0, 372, 374, 1, 0, 0, 0, 373, 371, 1, 0, 0, 0, 374, 375, 5, 34, 0, 0, 375, 126, 1, 0, 0, 0, 376, 382, 5, 96, 0, 0, 377, 378, 5, 92, 0, 0, 378, 381, 5, 96, 0, 0, 379, 381, 8, 2, 0, 0, 380, 377, 1, 0, 0, 0, 380, 379, 1, 0, 0, 0, 381, 384, 1, 0, 0, 0, 382, 380, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 385, 1, 0, 0, 0, 384, 382, 1, 0, 0, 0, 385, 386, 5, 96, 0, 0, 386, 128, 1, 0, 0, 0, 387, 388, 5, 92, 0, 0, 388, 397, 7, 3, 0, 0, 389, 390, 5, 92, 0, 0, 390, 391, 5, 117, 0, 0, 391, 392, 1, 0, 0, 0, 392, 393, 7, 4, 0, 0, 393, 394, 7, 4, 0, 0, 394, 395, 7, 4, 0, 0, 395, 397, 7, 4, 0, 0, 396, 387, 1, 0, 0, 0, 396, 389, 1, 0, 0, 0, 397, 130, 1, 0, 0, 0, 398, 402, 7, 5, 0, 0, 399, 401, 7, 6, 0, 0, 400, 399, 1, 0, 0, 0, 401, 404, 1, 0, 0, 0, 402, 400, 1, 0, 0, 0, 402, 403, 1, 0, 0, 0, 403, 132, 1, 0, 0, 0, 404, 402, 1, 0, 0, 0, 405, 406, 5, 47, 0, 0, 406, 407, 5, 47, 0, 0, 407, 411, 1, 0, 0, 0, 408, 410, 8, 7, 0, 0, 409, 408, 1, 0, 0, 0, 410, 413, 1, 0, 0, 0, 411, 409, 1, 0, 0, 0, 411, 412, 1, 0, 0, 0, 412, 414, 1, 0, 0, 0, 413, 411, 1, 0, 0, 0, 414, 415, 6, 66, 0, 0, 415, 134, 1, 0, 0, 0, 416, 417, 5, 47, 0, 0, 417, 418, 5, 42, 0, 0, 418, 422, 1, 0, 0, 0, 419, 421, 9, 0, 0, 0, 420, 419, 1, 0, 0, 0, 421, 424, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 422, 420, 1, 0, 0, 0, 423, 425, 1, 0, 0, 0, 424, 422, 1, 0, 0, 0, 425, 426, 5, 42, 0, 0, 426, 427, 5, 47, 0, 0, 427, 428, 1, 0, 0, 0, 428, 429, 6, 67, 0, 0, 429, 136, 1, 0, 0, 0, 430, 432, 7, 8, 0, 0, 431, 430, 1, 0, 0, 0, 432, 433, 1, 0, 0, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 435, 1, 0, 0, 0, 435, 436, 6, 68, 0, 0, 436, 138, 1, 0, 0, 0, 13, 0, 351, 360, 364, 369, 371, 380, 382, 396, 402, 411, 422, 433, 1, 0, 1, 0] \ No newline at end of file +[4, 0, 66, 440, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 1, 37, 1, 38, 1, 38, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 50, 1, 50, 1, 51, 1, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 4, 58, 350, 8, 58, 11, 58, 12, 58, 351, 1, 59, 1, 59, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 3, 61, 361, 8, 61, 1, 61, 1, 61, 3, 61, 365, 8, 61, 1, 62, 3, 62, 368, 8, 62, 1, 62, 1, 62, 1, 62, 1, 62, 5, 62, 374, 8, 62, 10, 62, 12, 62, 377, 9, 62, 1, 62, 1, 62, 1, 63, 3, 63, 382, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 5, 63, 388, 8, 63, 10, 63, 12, 63, 391, 9, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 400, 8, 64, 1, 65, 1, 65, 5, 65, 404, 8, 65, 10, 65, 12, 65, 407, 9, 65, 1, 66, 1, 66, 1, 66, 1, 66, 5, 66, 413, 8, 66, 10, 66, 12, 66, 416, 9, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 424, 8, 67, 10, 67, 12, 67, 427, 9, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 4, 68, 435, 8, 68, 11, 68, 12, 68, 436, 1, 68, 1, 68, 1, 425, 0, 69, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, 65, 33, 67, 34, 69, 35, 71, 36, 73, 37, 75, 38, 77, 39, 79, 40, 81, 41, 83, 42, 85, 43, 87, 44, 89, 45, 91, 46, 93, 47, 95, 48, 97, 49, 99, 50, 101, 51, 103, 52, 105, 53, 107, 54, 109, 55, 111, 56, 113, 57, 115, 58, 117, 0, 119, 0, 121, 59, 123, 60, 125, 61, 127, 62, 129, 0, 131, 63, 133, 64, 135, 65, 137, 66, 1, 0, 8, 1, 0, 48, 57, 2, 0, 92, 92, 96, 96, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 2, 0, 102, 102, 114, 114, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 451, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 89, 1, 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 95, 1, 0, 0, 0, 0, 97, 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, 0, 0, 0, 0, 103, 1, 0, 0, 0, 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, 109, 1, 0, 0, 0, 0, 111, 1, 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 115, 1, 0, 0, 0, 0, 121, 1, 0, 0, 0, 0, 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 131, 1, 0, 0, 0, 0, 133, 1, 0, 0, 0, 0, 135, 1, 0, 0, 0, 0, 137, 1, 0, 0, 0, 1, 139, 1, 0, 0, 0, 3, 144, 1, 0, 0, 0, 5, 147, 1, 0, 0, 0, 7, 152, 1, 0, 0, 0, 9, 159, 1, 0, 0, 0, 11, 163, 1, 0, 0, 0, 13, 169, 1, 0, 0, 0, 15, 178, 1, 0, 0, 0, 17, 187, 1, 0, 0, 0, 19, 193, 1, 0, 0, 0, 21, 198, 1, 0, 0, 0, 23, 203, 1, 0, 0, 0, 25, 213, 1, 0, 0, 0, 27, 216, 1, 0, 0, 0, 29, 220, 1, 0, 0, 0, 31, 224, 1, 0, 0, 0, 33, 228, 1, 0, 0, 0, 35, 231, 1, 0, 0, 0, 37, 235, 1, 0, 0, 0, 39, 239, 1, 0, 0, 0, 41, 243, 1, 0, 0, 0, 43, 247, 1, 0, 0, 0, 45, 251, 1, 0, 0, 0, 47, 255, 1, 0, 0, 0, 49, 262, 1, 0, 0, 0, 51, 265, 1, 0, 0, 0, 53, 268, 1, 0, 0, 0, 55, 271, 1, 0, 0, 0, 57, 274, 1, 0, 0, 0, 59, 276, 1, 0, 0, 0, 61, 279, 1, 0, 0, 0, 63, 282, 1, 0, 0, 0, 65, 285, 1, 0, 0, 0, 67, 288, 1, 0, 0, 0, 69, 291, 1, 0, 0, 0, 71, 293, 1, 0, 0, 0, 73, 295, 1, 0, 0, 0, 75, 297, 1, 0, 0, 0, 77, 299, 1, 0, 0, 0, 79, 301, 1, 0, 0, 0, 81, 303, 1, 0, 0, 0, 83, 306, 1, 0, 0, 0, 85, 309, 1, 0, 0, 0, 87, 311, 1, 0, 0, 0, 89, 313, 1, 0, 0, 0, 91, 316, 1, 0, 0, 0, 93, 319, 1, 0, 0, 0, 95, 323, 1, 0, 0, 0, 97, 326, 1, 0, 0, 0, 99, 330, 1, 0, 0, 0, 101, 332, 1, 0, 0, 0, 103, 334, 1, 0, 0, 0, 105, 336, 1, 0, 0, 0, 107, 338, 1, 0, 0, 0, 109, 340, 1, 0, 0, 0, 111, 342, 1, 0, 0, 0, 113, 344, 1, 0, 0, 0, 115, 346, 1, 0, 0, 0, 117, 349, 1, 0, 0, 0, 119, 353, 1, 0, 0, 0, 121, 355, 1, 0, 0, 0, 123, 364, 1, 0, 0, 0, 125, 367, 1, 0, 0, 0, 127, 381, 1, 0, 0, 0, 129, 399, 1, 0, 0, 0, 131, 401, 1, 0, 0, 0, 133, 408, 1, 0, 0, 0, 135, 419, 1, 0, 0, 0, 137, 434, 1, 0, 0, 0, 139, 140, 5, 102, 0, 0, 140, 141, 5, 117, 0, 0, 141, 142, 5, 110, 0, 0, 142, 143, 5, 99, 0, 0, 143, 2, 1, 0, 0, 0, 144, 145, 5, 105, 0, 0, 145, 146, 5, 102, 0, 0, 146, 4, 1, 0, 0, 0, 147, 148, 5, 101, 0, 0, 148, 149, 5, 108, 0, 0, 149, 150, 5, 115, 0, 0, 150, 151, 5, 101, 0, 0, 151, 6, 1, 0, 0, 0, 152, 153, 5, 114, 0, 0, 153, 154, 5, 101, 0, 0, 154, 155, 5, 116, 0, 0, 155, 156, 5, 117, 0, 0, 156, 157, 5, 114, 0, 0, 157, 158, 5, 110, 0, 0, 158, 8, 1, 0, 0, 0, 159, 160, 5, 102, 0, 0, 160, 161, 5, 111, 0, 0, 161, 162, 5, 114, 0, 0, 162, 10, 1, 0, 0, 0, 163, 164, 5, 98, 0, 0, 164, 165, 5, 114, 0, 0, 165, 166, 5, 101, 0, 0, 166, 167, 5, 97, 0, 0, 167, 168, 5, 107, 0, 0, 168, 12, 1, 0, 0, 0, 169, 170, 5, 99, 0, 0, 170, 171, 5, 111, 0, 0, 171, 172, 5, 110, 0, 0, 172, 173, 5, 116, 0, 0, 173, 174, 5, 105, 0, 0, 174, 175, 5, 110, 0, 0, 175, 176, 5, 117, 0, 0, 176, 177, 5, 101, 0, 0, 177, 14, 1, 0, 0, 0, 178, 179, 5, 115, 0, 0, 179, 180, 5, 101, 0, 0, 180, 181, 5, 113, 0, 0, 181, 182, 5, 117, 0, 0, 182, 183, 5, 101, 0, 0, 183, 184, 5, 110, 0, 0, 184, 185, 5, 99, 0, 0, 185, 186, 5, 101, 0, 0, 186, 16, 1, 0, 0, 0, 187, 188, 5, 115, 0, 0, 188, 189, 5, 116, 0, 0, 189, 190, 5, 97, 0, 0, 190, 191, 5, 103, 0, 0, 191, 192, 5, 101, 0, 0, 192, 18, 1, 0, 0, 0, 193, 194, 5, 110, 0, 0, 194, 195, 5, 101, 0, 0, 195, 196, 5, 120, 0, 0, 196, 197, 5, 116, 0, 0, 197, 20, 1, 0, 0, 0, 198, 199, 5, 99, 0, 0, 199, 200, 5, 104, 0, 0, 200, 201, 5, 97, 0, 0, 201, 202, 5, 110, 0, 0, 202, 22, 1, 0, 0, 0, 203, 204, 5, 97, 0, 0, 204, 205, 5, 117, 0, 0, 205, 206, 5, 116, 0, 0, 206, 207, 5, 104, 0, 0, 207, 208, 5, 111, 0, 0, 208, 209, 5, 114, 0, 0, 209, 210, 5, 105, 0, 0, 210, 211, 5, 116, 0, 0, 211, 212, 5, 121, 0, 0, 212, 24, 1, 0, 0, 0, 213, 214, 5, 105, 0, 0, 214, 215, 5, 56, 0, 0, 215, 26, 1, 0, 0, 0, 216, 217, 5, 105, 0, 0, 217, 218, 5, 49, 0, 0, 218, 219, 5, 54, 0, 0, 219, 28, 1, 0, 0, 0, 220, 221, 5, 105, 0, 0, 221, 222, 5, 51, 0, 0, 222, 223, 5, 50, 0, 0, 223, 30, 1, 0, 0, 0, 224, 225, 5, 105, 0, 0, 225, 226, 5, 54, 0, 0, 226, 227, 5, 52, 0, 0, 227, 32, 1, 0, 0, 0, 228, 229, 5, 117, 0, 0, 229, 230, 5, 56, 0, 0, 230, 34, 1, 0, 0, 0, 231, 232, 5, 117, 0, 0, 232, 233, 5, 49, 0, 0, 233, 234, 5, 54, 0, 0, 234, 36, 1, 0, 0, 0, 235, 236, 5, 117, 0, 0, 236, 237, 5, 51, 0, 0, 237, 238, 5, 50, 0, 0, 238, 38, 1, 0, 0, 0, 239, 240, 5, 117, 0, 0, 240, 241, 5, 54, 0, 0, 241, 242, 5, 52, 0, 0, 242, 40, 1, 0, 0, 0, 243, 244, 5, 102, 0, 0, 244, 245, 5, 51, 0, 0, 245, 246, 5, 50, 0, 0, 246, 42, 1, 0, 0, 0, 247, 248, 5, 102, 0, 0, 248, 249, 5, 54, 0, 0, 249, 250, 5, 52, 0, 0, 250, 44, 1, 0, 0, 0, 251, 252, 5, 115, 0, 0, 252, 253, 5, 116, 0, 0, 253, 254, 5, 114, 0, 0, 254, 46, 1, 0, 0, 0, 255, 256, 5, 115, 0, 0, 256, 257, 5, 101, 0, 0, 257, 258, 5, 114, 0, 0, 258, 259, 5, 105, 0, 0, 259, 260, 5, 101, 0, 0, 260, 261, 5, 115, 0, 0, 261, 48, 1, 0, 0, 0, 262, 263, 5, 45, 0, 0, 263, 264, 5, 62, 0, 0, 264, 50, 1, 0, 0, 0, 265, 266, 5, 58, 0, 0, 266, 267, 5, 61, 0, 0, 267, 52, 1, 0, 0, 0, 268, 269, 5, 36, 0, 0, 269, 270, 5, 61, 0, 0, 270, 54, 1, 0, 0, 0, 271, 272, 5, 61, 0, 0, 272, 273, 5, 62, 0, 0, 273, 56, 1, 0, 0, 0, 274, 275, 5, 61, 0, 0, 275, 58, 1, 0, 0, 0, 276, 277, 5, 43, 0, 0, 277, 278, 5, 61, 0, 0, 278, 60, 1, 0, 0, 0, 279, 280, 5, 45, 0, 0, 280, 281, 5, 61, 0, 0, 281, 62, 1, 0, 0, 0, 282, 283, 5, 42, 0, 0, 283, 284, 5, 61, 0, 0, 284, 64, 1, 0, 0, 0, 285, 286, 5, 47, 0, 0, 286, 287, 5, 61, 0, 0, 287, 66, 1, 0, 0, 0, 288, 289, 5, 37, 0, 0, 289, 290, 5, 61, 0, 0, 290, 68, 1, 0, 0, 0, 291, 292, 5, 43, 0, 0, 292, 70, 1, 0, 0, 0, 293, 294, 5, 45, 0, 0, 294, 72, 1, 0, 0, 0, 295, 296, 5, 42, 0, 0, 296, 74, 1, 0, 0, 0, 297, 298, 5, 47, 0, 0, 298, 76, 1, 0, 0, 0, 299, 300, 5, 37, 0, 0, 300, 78, 1, 0, 0, 0, 301, 302, 5, 94, 0, 0, 302, 80, 1, 0, 0, 0, 303, 304, 5, 61, 0, 0, 304, 305, 5, 61, 0, 0, 305, 82, 1, 0, 0, 0, 306, 307, 5, 33, 0, 0, 307, 308, 5, 61, 0, 0, 308, 84, 1, 0, 0, 0, 309, 310, 5, 60, 0, 0, 310, 86, 1, 0, 0, 0, 311, 312, 5, 62, 0, 0, 312, 88, 1, 0, 0, 0, 313, 314, 5, 60, 0, 0, 314, 315, 5, 61, 0, 0, 315, 90, 1, 0, 0, 0, 316, 317, 5, 62, 0, 0, 317, 318, 5, 61, 0, 0, 318, 92, 1, 0, 0, 0, 319, 320, 5, 97, 0, 0, 320, 321, 5, 110, 0, 0, 321, 322, 5, 100, 0, 0, 322, 94, 1, 0, 0, 0, 323, 324, 5, 111, 0, 0, 324, 325, 5, 114, 0, 0, 325, 96, 1, 0, 0, 0, 326, 327, 5, 110, 0, 0, 327, 328, 5, 111, 0, 0, 328, 329, 5, 116, 0, 0, 329, 98, 1, 0, 0, 0, 330, 331, 5, 40, 0, 0, 331, 100, 1, 0, 0, 0, 332, 333, 5, 41, 0, 0, 333, 102, 1, 0, 0, 0, 334, 335, 5, 123, 0, 0, 335, 104, 1, 0, 0, 0, 336, 337, 5, 125, 0, 0, 337, 106, 1, 0, 0, 0, 338, 339, 5, 91, 0, 0, 339, 108, 1, 0, 0, 0, 340, 341, 5, 93, 0, 0, 341, 110, 1, 0, 0, 0, 342, 343, 5, 44, 0, 0, 343, 112, 1, 0, 0, 0, 344, 345, 5, 58, 0, 0, 345, 114, 1, 0, 0, 0, 346, 347, 5, 46, 0, 0, 347, 116, 1, 0, 0, 0, 348, 350, 3, 119, 59, 0, 349, 348, 1, 0, 0, 0, 350, 351, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, 118, 1, 0, 0, 0, 353, 354, 7, 0, 0, 0, 354, 120, 1, 0, 0, 0, 355, 356, 3, 117, 58, 0, 356, 122, 1, 0, 0, 0, 357, 358, 3, 117, 58, 0, 358, 360, 5, 46, 0, 0, 359, 361, 3, 117, 58, 0, 360, 359, 1, 0, 0, 0, 360, 361, 1, 0, 0, 0, 361, 365, 1, 0, 0, 0, 362, 363, 5, 46, 0, 0, 363, 365, 3, 117, 58, 0, 364, 357, 1, 0, 0, 0, 364, 362, 1, 0, 0, 0, 365, 124, 1, 0, 0, 0, 366, 368, 3, 129, 64, 0, 367, 366, 1, 0, 0, 0, 367, 368, 1, 0, 0, 0, 368, 369, 1, 0, 0, 0, 369, 375, 5, 96, 0, 0, 370, 374, 8, 1, 0, 0, 371, 372, 5, 92, 0, 0, 372, 374, 9, 0, 0, 0, 373, 370, 1, 0, 0, 0, 373, 371, 1, 0, 0, 0, 374, 377, 1, 0, 0, 0, 375, 373, 1, 0, 0, 0, 375, 376, 1, 0, 0, 0, 376, 378, 1, 0, 0, 0, 377, 375, 1, 0, 0, 0, 378, 379, 5, 96, 0, 0, 379, 126, 1, 0, 0, 0, 380, 382, 3, 129, 64, 0, 381, 380, 1, 0, 0, 0, 381, 382, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 389, 5, 34, 0, 0, 384, 388, 8, 2, 0, 0, 385, 386, 5, 92, 0, 0, 386, 388, 9, 0, 0, 0, 387, 384, 1, 0, 0, 0, 387, 385, 1, 0, 0, 0, 388, 391, 1, 0, 0, 0, 389, 387, 1, 0, 0, 0, 389, 390, 1, 0, 0, 0, 390, 392, 1, 0, 0, 0, 391, 389, 1, 0, 0, 0, 392, 393, 5, 34, 0, 0, 393, 128, 1, 0, 0, 0, 394, 400, 7, 3, 0, 0, 395, 396, 5, 114, 0, 0, 396, 400, 5, 102, 0, 0, 397, 398, 5, 102, 0, 0, 398, 400, 5, 114, 0, 0, 399, 394, 1, 0, 0, 0, 399, 395, 1, 0, 0, 0, 399, 397, 1, 0, 0, 0, 400, 130, 1, 0, 0, 0, 401, 405, 7, 4, 0, 0, 402, 404, 7, 5, 0, 0, 403, 402, 1, 0, 0, 0, 404, 407, 1, 0, 0, 0, 405, 403, 1, 0, 0, 0, 405, 406, 1, 0, 0, 0, 406, 132, 1, 0, 0, 0, 407, 405, 1, 0, 0, 0, 408, 409, 5, 47, 0, 0, 409, 410, 5, 47, 0, 0, 410, 414, 1, 0, 0, 0, 411, 413, 8, 6, 0, 0, 412, 411, 1, 0, 0, 0, 413, 416, 1, 0, 0, 0, 414, 412, 1, 0, 0, 0, 414, 415, 1, 0, 0, 0, 415, 417, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 417, 418, 6, 66, 0, 0, 418, 134, 1, 0, 0, 0, 419, 420, 5, 47, 0, 0, 420, 421, 5, 42, 0, 0, 421, 425, 1, 0, 0, 0, 422, 424, 9, 0, 0, 0, 423, 422, 1, 0, 0, 0, 424, 427, 1, 0, 0, 0, 425, 426, 1, 0, 0, 0, 425, 423, 1, 0, 0, 0, 426, 428, 1, 0, 0, 0, 427, 425, 1, 0, 0, 0, 428, 429, 5, 42, 0, 0, 429, 430, 5, 47, 0, 0, 430, 431, 1, 0, 0, 0, 431, 432, 6, 67, 0, 0, 432, 136, 1, 0, 0, 0, 433, 435, 7, 7, 0, 0, 434, 433, 1, 0, 0, 0, 435, 436, 1, 0, 0, 0, 436, 434, 1, 0, 0, 0, 436, 437, 1, 0, 0, 0, 437, 438, 1, 0, 0, 0, 438, 439, 6, 68, 0, 0, 439, 138, 1, 0, 0, 0, 15, 0, 351, 360, 364, 367, 373, 375, 381, 387, 389, 399, 405, 414, 425, 436, 1, 0, 1, 0] \ No newline at end of file diff --git a/arc/go/parser/ArcLexer.tokens b/arc/go/parser/ArcLexer.tokens index 692915c751..201c4aec50 100644 --- a/arc/go/parser/ArcLexer.tokens +++ b/arc/go/parser/ArcLexer.tokens @@ -58,8 +58,8 @@ COLON=57 DOT=58 INTEGER_LITERAL=59 FLOAT_LITERAL=60 -STR_LITERAL=61 -STR_LITERAL_RAW=62 +STR_LITERAL_MULTI=61 +STR_LITERAL=62 IDENTIFIER=63 SINGLE_LINE_COMMENT=64 MULTI_LINE_COMMENT=65 diff --git a/arc/go/parser/ArcParser.g4 b/arc/go/parser/ArcParser.g4 index cc562636c4..5d1cf4fbcb 100644 --- a/arc/go/parser/ArcParser.g4 +++ b/arc/go/parser/ArcParser.g4 @@ -407,7 +407,7 @@ typeCast literal : numericLiteral | STR_LITERAL - | STR_LITERAL_RAW + | STR_LITERAL_MULTI | seriesLiteral ; diff --git a/arc/go/parser/ArcParser.interp b/arc/go/parser/ArcParser.interp index 1d6d667b63..684e06ed66 100644 --- a/arc/go/parser/ArcParser.interp +++ b/arc/go/parser/ArcParser.interp @@ -129,8 +129,8 @@ COLON DOT INTEGER_LITERAL FLOAT_LITERAL +STR_LITERAL_MULTI STR_LITERAL -STR_LITERAL_RAW IDENTIFIER SINGLE_LINE_COMMENT MULTI_LINE_COMMENT @@ -215,4 +215,4 @@ expressionList atn: -[4, 1, 66, 739, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 1, 0, 5, 0, 152, 8, 0, 10, 0, 12, 0, 155, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 165, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 172, 8, 2, 10, 2, 12, 2, 175, 9, 2, 1, 2, 3, 2, 178, 8, 2, 1, 3, 1, 3, 1, 3, 3, 3, 183, 8, 3, 1, 4, 1, 4, 1, 4, 3, 4, 188, 8, 4, 1, 4, 1, 4, 3, 4, 192, 8, 4, 1, 4, 1, 4, 3, 4, 196, 8, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 203, 8, 5, 10, 5, 12, 5, 206, 9, 5, 1, 5, 3, 5, 209, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 215, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 221, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 5, 8, 227, 8, 8, 10, 8, 12, 8, 230, 9, 8, 1, 8, 3, 8, 233, 8, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 3, 10, 242, 8, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 5, 11, 249, 8, 11, 10, 11, 12, 11, 252, 9, 11, 1, 11, 3, 11, 255, 8, 11, 1, 12, 1, 12, 1, 12, 1, 12, 3, 12, 261, 8, 12, 1, 13, 1, 13, 3, 13, 265, 8, 13, 1, 13, 1, 13, 1, 13, 3, 13, 270, 8, 13, 1, 13, 5, 13, 273, 8, 13, 10, 13, 12, 13, 276, 9, 13, 1, 13, 3, 13, 279, 8, 13, 3, 13, 281, 8, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 3, 14, 289, 8, 14, 1, 15, 1, 15, 3, 15, 293, 8, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 3, 16, 300, 8, 16, 1, 16, 5, 16, 303, 8, 16, 10, 16, 12, 16, 306, 9, 16, 1, 16, 3, 16, 309, 8, 16, 3, 16, 311, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 3, 17, 318, 8, 17, 1, 18, 1, 18, 3, 18, 322, 8, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 3, 19, 332, 8, 19, 1, 20, 1, 20, 3, 20, 336, 8, 20, 1, 20, 1, 20, 1, 20, 3, 20, 341, 8, 20, 4, 20, 343, 8, 20, 11, 20, 12, 20, 344, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 5, 22, 353, 8, 22, 10, 22, 12, 22, 356, 9, 22, 1, 22, 3, 22, 359, 8, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 5, 23, 368, 8, 23, 10, 23, 12, 23, 371, 9, 23, 1, 23, 1, 23, 3, 23, 375, 8, 23, 1, 24, 1, 24, 1, 24, 1, 24, 3, 24, 381, 8, 24, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 390, 8, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 401, 8, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 3, 28, 413, 8, 28, 1, 29, 1, 29, 1, 29, 5, 29, 418, 8, 29, 10, 29, 12, 29, 421, 9, 29, 1, 29, 3, 29, 424, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 5, 31, 433, 8, 31, 10, 31, 12, 31, 436, 9, 31, 1, 31, 3, 31, 439, 8, 31, 1, 32, 1, 32, 3, 32, 443, 8, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 5, 33, 450, 8, 33, 10, 33, 12, 33, 453, 9, 33, 1, 33, 3, 33, 456, 8, 33, 1, 34, 1, 34, 5, 34, 460, 8, 34, 10, 34, 12, 34, 463, 9, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 3, 35, 475, 8, 35, 1, 36, 1, 36, 3, 36, 479, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 489, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 499, 8, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 518, 8, 39, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 5, 41, 526, 8, 41, 10, 41, 12, 41, 529, 9, 41, 1, 41, 3, 41, 532, 8, 41, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 3, 45, 556, 8, 45, 1, 46, 1, 46, 1, 47, 1, 47, 1, 48, 1, 48, 3, 48, 564, 8, 48, 1, 49, 1, 49, 3, 49, 568, 8, 49, 1, 49, 1, 49, 3, 49, 572, 8, 49, 1, 50, 1, 50, 1, 51, 1, 51, 3, 51, 578, 8, 51, 1, 52, 1, 52, 3, 52, 582, 8, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 3, 55, 591, 8, 55, 1, 55, 1, 55, 3, 55, 595, 8, 55, 1, 56, 1, 56, 1, 56, 3, 56, 600, 8, 56, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 5, 58, 607, 8, 58, 10, 58, 12, 58, 610, 9, 58, 1, 59, 1, 59, 1, 59, 5, 59, 615, 8, 59, 10, 59, 12, 59, 618, 9, 59, 1, 60, 1, 60, 1, 60, 5, 60, 623, 8, 60, 10, 60, 12, 60, 626, 9, 60, 1, 61, 1, 61, 1, 61, 5, 61, 631, 8, 61, 10, 61, 12, 61, 634, 9, 61, 1, 62, 1, 62, 1, 62, 5, 62, 639, 8, 62, 10, 62, 12, 62, 642, 9, 62, 1, 63, 1, 63, 1, 63, 5, 63, 647, 8, 63, 10, 63, 12, 63, 650, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 655, 8, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 662, 8, 65, 1, 66, 1, 66, 1, 66, 5, 66, 667, 8, 66, 10, 66, 12, 66, 670, 9, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 3, 67, 678, 8, 67, 1, 67, 1, 67, 3, 67, 682, 8, 67, 1, 67, 3, 67, 685, 8, 67, 1, 68, 1, 68, 3, 68, 689, 8, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 701, 8, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 3, 71, 712, 8, 71, 1, 72, 3, 72, 715, 8, 72, 1, 72, 1, 72, 1, 72, 3, 72, 720, 8, 72, 1, 73, 1, 73, 3, 73, 724, 8, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 5, 74, 731, 8, 74, 10, 74, 12, 74, 734, 9, 74, 1, 74, 3, 74, 737, 8, 74, 1, 74, 0, 0, 75, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 0, 9, 2, 0, 25, 25, 28, 28, 1, 0, 30, 34, 1, 0, 13, 20, 1, 0, 21, 22, 1, 0, 41, 42, 1, 0, 43, 46, 1, 0, 35, 36, 1, 0, 37, 39, 1, 0, 59, 60, 780, 0, 153, 1, 0, 0, 0, 2, 164, 1, 0, 0, 0, 4, 177, 1, 0, 0, 0, 6, 182, 1, 0, 0, 0, 8, 184, 1, 0, 0, 0, 10, 199, 1, 0, 0, 0, 12, 210, 1, 0, 0, 0, 14, 220, 1, 0, 0, 0, 16, 222, 1, 0, 0, 0, 18, 236, 1, 0, 0, 0, 20, 239, 1, 0, 0, 0, 22, 245, 1, 0, 0, 0, 24, 256, 1, 0, 0, 0, 26, 262, 1, 0, 0, 0, 28, 288, 1, 0, 0, 0, 30, 290, 1, 0, 0, 0, 32, 296, 1, 0, 0, 0, 34, 317, 1, 0, 0, 0, 36, 321, 1, 0, 0, 0, 38, 331, 1, 0, 0, 0, 40, 335, 1, 0, 0, 0, 42, 346, 1, 0, 0, 0, 44, 348, 1, 0, 0, 0, 46, 362, 1, 0, 0, 0, 48, 380, 1, 0, 0, 0, 50, 382, 1, 0, 0, 0, 52, 389, 1, 0, 0, 0, 54, 400, 1, 0, 0, 0, 56, 412, 1, 0, 0, 0, 58, 414, 1, 0, 0, 0, 60, 425, 1, 0, 0, 0, 62, 429, 1, 0, 0, 0, 64, 440, 1, 0, 0, 0, 66, 446, 1, 0, 0, 0, 68, 457, 1, 0, 0, 0, 70, 474, 1, 0, 0, 0, 72, 478, 1, 0, 0, 0, 74, 488, 1, 0, 0, 0, 76, 498, 1, 0, 0, 0, 78, 517, 1, 0, 0, 0, 80, 519, 1, 0, 0, 0, 82, 521, 1, 0, 0, 0, 84, 533, 1, 0, 0, 0, 86, 538, 1, 0, 0, 0, 88, 541, 1, 0, 0, 0, 90, 555, 1, 0, 0, 0, 92, 557, 1, 0, 0, 0, 94, 559, 1, 0, 0, 0, 96, 561, 1, 0, 0, 0, 98, 571, 1, 0, 0, 0, 100, 573, 1, 0, 0, 0, 102, 577, 1, 0, 0, 0, 104, 581, 1, 0, 0, 0, 106, 583, 1, 0, 0, 0, 108, 585, 1, 0, 0, 0, 110, 594, 1, 0, 0, 0, 112, 596, 1, 0, 0, 0, 114, 601, 1, 0, 0, 0, 116, 603, 1, 0, 0, 0, 118, 611, 1, 0, 0, 0, 120, 619, 1, 0, 0, 0, 122, 627, 1, 0, 0, 0, 124, 635, 1, 0, 0, 0, 126, 643, 1, 0, 0, 0, 128, 651, 1, 0, 0, 0, 130, 661, 1, 0, 0, 0, 132, 663, 1, 0, 0, 0, 134, 684, 1, 0, 0, 0, 136, 686, 1, 0, 0, 0, 138, 700, 1, 0, 0, 0, 140, 702, 1, 0, 0, 0, 142, 711, 1, 0, 0, 0, 144, 714, 1, 0, 0, 0, 146, 721, 1, 0, 0, 0, 148, 727, 1, 0, 0, 0, 150, 152, 3, 2, 1, 0, 151, 150, 1, 0, 0, 0, 152, 155, 1, 0, 0, 0, 153, 151, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 156, 1, 0, 0, 0, 155, 153, 1, 0, 0, 0, 156, 157, 5, 0, 0, 1, 157, 1, 1, 0, 0, 0, 158, 165, 3, 4, 2, 0, 159, 165, 3, 8, 4, 0, 160, 165, 3, 40, 20, 0, 161, 165, 3, 26, 13, 0, 162, 165, 3, 30, 15, 0, 163, 165, 3, 38, 19, 0, 164, 158, 1, 0, 0, 0, 164, 159, 1, 0, 0, 0, 164, 160, 1, 0, 0, 0, 164, 161, 1, 0, 0, 0, 164, 162, 1, 0, 0, 0, 164, 163, 1, 0, 0, 0, 165, 3, 1, 0, 0, 0, 166, 167, 5, 12, 0, 0, 167, 178, 5, 59, 0, 0, 168, 169, 5, 12, 0, 0, 169, 173, 5, 50, 0, 0, 170, 172, 3, 6, 3, 0, 171, 170, 1, 0, 0, 0, 172, 175, 1, 0, 0, 0, 173, 171, 1, 0, 0, 0, 173, 174, 1, 0, 0, 0, 174, 176, 1, 0, 0, 0, 175, 173, 1, 0, 0, 0, 176, 178, 5, 51, 0, 0, 177, 166, 1, 0, 0, 0, 177, 168, 1, 0, 0, 0, 178, 5, 1, 0, 0, 0, 179, 183, 5, 59, 0, 0, 180, 181, 5, 63, 0, 0, 181, 183, 5, 59, 0, 0, 182, 179, 1, 0, 0, 0, 182, 180, 1, 0, 0, 0, 183, 7, 1, 0, 0, 0, 184, 185, 5, 1, 0, 0, 185, 187, 5, 63, 0, 0, 186, 188, 3, 20, 10, 0, 187, 186, 1, 0, 0, 0, 187, 188, 1, 0, 0, 0, 188, 189, 1, 0, 0, 0, 189, 191, 5, 50, 0, 0, 190, 192, 3, 10, 5, 0, 191, 190, 1, 0, 0, 0, 191, 192, 1, 0, 0, 0, 192, 193, 1, 0, 0, 0, 193, 195, 5, 51, 0, 0, 194, 196, 3, 14, 7, 0, 195, 194, 1, 0, 0, 0, 195, 196, 1, 0, 0, 0, 196, 197, 1, 0, 0, 0, 197, 198, 3, 68, 34, 0, 198, 9, 1, 0, 0, 0, 199, 204, 3, 12, 6, 0, 200, 201, 5, 56, 0, 0, 201, 203, 3, 12, 6, 0, 202, 200, 1, 0, 0, 0, 203, 206, 1, 0, 0, 0, 204, 202, 1, 0, 0, 0, 204, 205, 1, 0, 0, 0, 205, 208, 1, 0, 0, 0, 206, 204, 1, 0, 0, 0, 207, 209, 5, 56, 0, 0, 208, 207, 1, 0, 0, 0, 208, 209, 1, 0, 0, 0, 209, 11, 1, 0, 0, 0, 210, 211, 5, 63, 0, 0, 211, 214, 3, 98, 49, 0, 212, 213, 5, 29, 0, 0, 213, 215, 3, 142, 71, 0, 214, 212, 1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 13, 1, 0, 0, 0, 216, 221, 3, 98, 49, 0, 217, 218, 5, 63, 0, 0, 218, 221, 3, 98, 49, 0, 219, 221, 3, 16, 8, 0, 220, 216, 1, 0, 0, 0, 220, 217, 1, 0, 0, 0, 220, 219, 1, 0, 0, 0, 221, 15, 1, 0, 0, 0, 222, 223, 5, 50, 0, 0, 223, 228, 3, 18, 9, 0, 224, 225, 5, 56, 0, 0, 225, 227, 3, 18, 9, 0, 226, 224, 1, 0, 0, 0, 227, 230, 1, 0, 0, 0, 228, 226, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 232, 1, 0, 0, 0, 230, 228, 1, 0, 0, 0, 231, 233, 5, 56, 0, 0, 232, 231, 1, 0, 0, 0, 232, 233, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 5, 51, 0, 0, 235, 17, 1, 0, 0, 0, 236, 237, 5, 63, 0, 0, 237, 238, 3, 98, 49, 0, 238, 19, 1, 0, 0, 0, 239, 241, 5, 52, 0, 0, 240, 242, 3, 22, 11, 0, 241, 240, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 1, 0, 0, 0, 243, 244, 5, 53, 0, 0, 244, 21, 1, 0, 0, 0, 245, 250, 3, 24, 12, 0, 246, 247, 5, 56, 0, 0, 247, 249, 3, 24, 12, 0, 248, 246, 1, 0, 0, 0, 249, 252, 1, 0, 0, 0, 250, 248, 1, 0, 0, 0, 250, 251, 1, 0, 0, 0, 251, 254, 1, 0, 0, 0, 252, 250, 1, 0, 0, 0, 253, 255, 5, 56, 0, 0, 254, 253, 1, 0, 0, 0, 254, 255, 1, 0, 0, 0, 255, 23, 1, 0, 0, 0, 256, 257, 5, 63, 0, 0, 257, 260, 3, 98, 49, 0, 258, 259, 5, 29, 0, 0, 259, 261, 3, 142, 71, 0, 260, 258, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 25, 1, 0, 0, 0, 262, 264, 5, 8, 0, 0, 263, 265, 5, 63, 0, 0, 264, 263, 1, 0, 0, 0, 264, 265, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 280, 5, 52, 0, 0, 267, 274, 3, 28, 14, 0, 268, 270, 5, 56, 0, 0, 269, 268, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273, 3, 28, 14, 0, 272, 269, 1, 0, 0, 0, 273, 276, 1, 0, 0, 0, 274, 272, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 278, 1, 0, 0, 0, 276, 274, 1, 0, 0, 0, 277, 279, 5, 56, 0, 0, 278, 277, 1, 0, 0, 0, 278, 279, 1, 0, 0, 0, 279, 281, 1, 0, 0, 0, 280, 267, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 283, 5, 53, 0, 0, 283, 27, 1, 0, 0, 0, 284, 289, 3, 30, 15, 0, 285, 289, 3, 26, 13, 0, 286, 289, 3, 40, 20, 0, 287, 289, 3, 36, 18, 0, 288, 284, 1, 0, 0, 0, 288, 285, 1, 0, 0, 0, 288, 286, 1, 0, 0, 0, 288, 287, 1, 0, 0, 0, 289, 29, 1, 0, 0, 0, 290, 292, 5, 9, 0, 0, 291, 293, 5, 63, 0, 0, 292, 291, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 3, 32, 16, 0, 295, 31, 1, 0, 0, 0, 296, 310, 5, 52, 0, 0, 297, 304, 3, 34, 17, 0, 298, 300, 5, 56, 0, 0, 299, 298, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 303, 3, 34, 17, 0, 302, 299, 1, 0, 0, 0, 303, 306, 1, 0, 0, 0, 304, 302, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 308, 1, 0, 0, 0, 306, 304, 1, 0, 0, 0, 307, 309, 5, 56, 0, 0, 308, 307, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 297, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 1, 0, 0, 0, 312, 313, 5, 53, 0, 0, 313, 33, 1, 0, 0, 0, 314, 318, 3, 40, 20, 0, 315, 318, 3, 36, 18, 0, 316, 318, 3, 26, 13, 0, 317, 314, 1, 0, 0, 0, 317, 315, 1, 0, 0, 0, 317, 316, 1, 0, 0, 0, 318, 35, 1, 0, 0, 0, 319, 322, 3, 52, 26, 0, 320, 322, 3, 114, 57, 0, 321, 319, 1, 0, 0, 0, 321, 320, 1, 0, 0, 0, 322, 37, 1, 0, 0, 0, 323, 324, 5, 63, 0, 0, 324, 325, 5, 26, 0, 0, 325, 332, 3, 142, 71, 0, 326, 327, 5, 63, 0, 0, 327, 328, 3, 98, 49, 0, 328, 329, 5, 26, 0, 0, 329, 330, 3, 142, 71, 0, 330, 332, 1, 0, 0, 0, 331, 323, 1, 0, 0, 0, 331, 326, 1, 0, 0, 0, 332, 39, 1, 0, 0, 0, 333, 336, 3, 44, 22, 0, 334, 336, 3, 48, 24, 0, 335, 333, 1, 0, 0, 0, 335, 334, 1, 0, 0, 0, 336, 342, 1, 0, 0, 0, 337, 340, 3, 42, 21, 0, 338, 341, 3, 44, 22, 0, 339, 341, 3, 48, 24, 0, 340, 338, 1, 0, 0, 0, 340, 339, 1, 0, 0, 0, 341, 343, 1, 0, 0, 0, 342, 337, 1, 0, 0, 0, 343, 344, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 344, 345, 1, 0, 0, 0, 345, 41, 1, 0, 0, 0, 346, 347, 7, 0, 0, 0, 347, 43, 1, 0, 0, 0, 348, 349, 5, 52, 0, 0, 349, 354, 3, 46, 23, 0, 350, 351, 5, 56, 0, 0, 351, 353, 3, 46, 23, 0, 352, 350, 1, 0, 0, 0, 353, 356, 1, 0, 0, 0, 354, 352, 1, 0, 0, 0, 354, 355, 1, 0, 0, 0, 355, 358, 1, 0, 0, 0, 356, 354, 1, 0, 0, 0, 357, 359, 5, 56, 0, 0, 358, 357, 1, 0, 0, 0, 358, 359, 1, 0, 0, 0, 359, 360, 1, 0, 0, 0, 360, 361, 5, 53, 0, 0, 361, 45, 1, 0, 0, 0, 362, 363, 5, 63, 0, 0, 363, 364, 5, 57, 0, 0, 364, 369, 3, 48, 24, 0, 365, 366, 5, 25, 0, 0, 366, 368, 3, 48, 24, 0, 367, 365, 1, 0, 0, 0, 368, 371, 1, 0, 0, 0, 369, 367, 1, 0, 0, 0, 369, 370, 1, 0, 0, 0, 370, 374, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 372, 373, 5, 57, 0, 0, 373, 375, 5, 63, 0, 0, 374, 372, 1, 0, 0, 0, 374, 375, 1, 0, 0, 0, 375, 47, 1, 0, 0, 0, 376, 381, 3, 50, 25, 0, 377, 381, 3, 52, 26, 0, 378, 381, 3, 114, 57, 0, 379, 381, 5, 10, 0, 0, 380, 376, 1, 0, 0, 0, 380, 377, 1, 0, 0, 0, 380, 378, 1, 0, 0, 0, 380, 379, 1, 0, 0, 0, 381, 49, 1, 0, 0, 0, 382, 383, 5, 63, 0, 0, 383, 51, 1, 0, 0, 0, 384, 385, 3, 54, 27, 0, 385, 386, 3, 56, 28, 0, 386, 390, 1, 0, 0, 0, 387, 388, 5, 63, 0, 0, 388, 390, 3, 56, 28, 0, 389, 384, 1, 0, 0, 0, 389, 387, 1, 0, 0, 0, 390, 53, 1, 0, 0, 0, 391, 392, 5, 63, 0, 0, 392, 393, 5, 58, 0, 0, 393, 401, 5, 63, 0, 0, 394, 395, 5, 63, 0, 0, 395, 396, 5, 58, 0, 0, 396, 401, 5, 5, 0, 0, 397, 398, 5, 12, 0, 0, 398, 399, 5, 58, 0, 0, 399, 401, 5, 63, 0, 0, 400, 391, 1, 0, 0, 0, 400, 394, 1, 0, 0, 0, 400, 397, 1, 0, 0, 0, 401, 55, 1, 0, 0, 0, 402, 403, 5, 52, 0, 0, 403, 413, 5, 53, 0, 0, 404, 405, 5, 52, 0, 0, 405, 406, 3, 58, 29, 0, 406, 407, 5, 53, 0, 0, 407, 413, 1, 0, 0, 0, 408, 409, 5, 52, 0, 0, 409, 410, 3, 62, 31, 0, 410, 411, 5, 53, 0, 0, 411, 413, 1, 0, 0, 0, 412, 402, 1, 0, 0, 0, 412, 404, 1, 0, 0, 0, 412, 408, 1, 0, 0, 0, 413, 57, 1, 0, 0, 0, 414, 419, 3, 60, 30, 0, 415, 416, 5, 56, 0, 0, 416, 418, 3, 60, 30, 0, 417, 415, 1, 0, 0, 0, 418, 421, 1, 0, 0, 0, 419, 417, 1, 0, 0, 0, 419, 420, 1, 0, 0, 0, 420, 423, 1, 0, 0, 0, 421, 419, 1, 0, 0, 0, 422, 424, 5, 56, 0, 0, 423, 422, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 59, 1, 0, 0, 0, 425, 426, 5, 63, 0, 0, 426, 427, 5, 29, 0, 0, 427, 428, 3, 114, 57, 0, 428, 61, 1, 0, 0, 0, 429, 434, 3, 114, 57, 0, 430, 431, 5, 56, 0, 0, 431, 433, 3, 114, 57, 0, 432, 430, 1, 0, 0, 0, 433, 436, 1, 0, 0, 0, 434, 432, 1, 0, 0, 0, 434, 435, 1, 0, 0, 0, 435, 438, 1, 0, 0, 0, 436, 434, 1, 0, 0, 0, 437, 439, 5, 56, 0, 0, 438, 437, 1, 0, 0, 0, 438, 439, 1, 0, 0, 0, 439, 63, 1, 0, 0, 0, 440, 442, 5, 50, 0, 0, 441, 443, 3, 66, 33, 0, 442, 441, 1, 0, 0, 0, 442, 443, 1, 0, 0, 0, 443, 444, 1, 0, 0, 0, 444, 445, 5, 51, 0, 0, 445, 65, 1, 0, 0, 0, 446, 451, 3, 114, 57, 0, 447, 448, 5, 56, 0, 0, 448, 450, 3, 114, 57, 0, 449, 447, 1, 0, 0, 0, 450, 453, 1, 0, 0, 0, 451, 449, 1, 0, 0, 0, 451, 452, 1, 0, 0, 0, 452, 455, 1, 0, 0, 0, 453, 451, 1, 0, 0, 0, 454, 456, 5, 56, 0, 0, 455, 454, 1, 0, 0, 0, 455, 456, 1, 0, 0, 0, 456, 67, 1, 0, 0, 0, 457, 461, 5, 52, 0, 0, 458, 460, 3, 70, 35, 0, 459, 458, 1, 0, 0, 0, 460, 463, 1, 0, 0, 0, 461, 459, 1, 0, 0, 0, 461, 462, 1, 0, 0, 0, 462, 464, 1, 0, 0, 0, 463, 461, 1, 0, 0, 0, 464, 465, 5, 53, 0, 0, 465, 69, 1, 0, 0, 0, 466, 475, 3, 72, 36, 0, 467, 475, 3, 78, 39, 0, 468, 475, 3, 82, 41, 0, 469, 475, 3, 88, 44, 0, 470, 475, 3, 92, 46, 0, 471, 475, 3, 94, 47, 0, 472, 475, 3, 96, 48, 0, 473, 475, 3, 114, 57, 0, 474, 466, 1, 0, 0, 0, 474, 467, 1, 0, 0, 0, 474, 468, 1, 0, 0, 0, 474, 469, 1, 0, 0, 0, 474, 470, 1, 0, 0, 0, 474, 471, 1, 0, 0, 0, 474, 472, 1, 0, 0, 0, 474, 473, 1, 0, 0, 0, 475, 71, 1, 0, 0, 0, 476, 479, 3, 74, 37, 0, 477, 479, 3, 76, 38, 0, 478, 476, 1, 0, 0, 0, 478, 477, 1, 0, 0, 0, 479, 73, 1, 0, 0, 0, 480, 481, 5, 63, 0, 0, 481, 482, 5, 26, 0, 0, 482, 489, 3, 114, 57, 0, 483, 484, 5, 63, 0, 0, 484, 485, 3, 98, 49, 0, 485, 486, 5, 26, 0, 0, 486, 487, 3, 114, 57, 0, 487, 489, 1, 0, 0, 0, 488, 480, 1, 0, 0, 0, 488, 483, 1, 0, 0, 0, 489, 75, 1, 0, 0, 0, 490, 491, 5, 63, 0, 0, 491, 492, 5, 27, 0, 0, 492, 499, 3, 114, 57, 0, 493, 494, 5, 63, 0, 0, 494, 495, 3, 98, 49, 0, 495, 496, 5, 27, 0, 0, 496, 497, 3, 114, 57, 0, 497, 499, 1, 0, 0, 0, 498, 490, 1, 0, 0, 0, 498, 493, 1, 0, 0, 0, 499, 77, 1, 0, 0, 0, 500, 501, 5, 63, 0, 0, 501, 502, 5, 29, 0, 0, 502, 518, 3, 114, 57, 0, 503, 504, 5, 63, 0, 0, 504, 505, 3, 134, 67, 0, 505, 506, 5, 29, 0, 0, 506, 507, 3, 114, 57, 0, 507, 518, 1, 0, 0, 0, 508, 509, 5, 63, 0, 0, 509, 510, 3, 80, 40, 0, 510, 511, 3, 114, 57, 0, 511, 518, 1, 0, 0, 0, 512, 513, 5, 63, 0, 0, 513, 514, 3, 134, 67, 0, 514, 515, 3, 80, 40, 0, 515, 516, 3, 114, 57, 0, 516, 518, 1, 0, 0, 0, 517, 500, 1, 0, 0, 0, 517, 503, 1, 0, 0, 0, 517, 508, 1, 0, 0, 0, 517, 512, 1, 0, 0, 0, 518, 79, 1, 0, 0, 0, 519, 520, 7, 1, 0, 0, 520, 81, 1, 0, 0, 0, 521, 522, 5, 2, 0, 0, 522, 523, 3, 114, 57, 0, 523, 527, 3, 68, 34, 0, 524, 526, 3, 84, 42, 0, 525, 524, 1, 0, 0, 0, 526, 529, 1, 0, 0, 0, 527, 525, 1, 0, 0, 0, 527, 528, 1, 0, 0, 0, 528, 531, 1, 0, 0, 0, 529, 527, 1, 0, 0, 0, 530, 532, 3, 86, 43, 0, 531, 530, 1, 0, 0, 0, 531, 532, 1, 0, 0, 0, 532, 83, 1, 0, 0, 0, 533, 534, 5, 3, 0, 0, 534, 535, 5, 2, 0, 0, 535, 536, 3, 114, 57, 0, 536, 537, 3, 68, 34, 0, 537, 85, 1, 0, 0, 0, 538, 539, 5, 3, 0, 0, 539, 540, 3, 68, 34, 0, 540, 87, 1, 0, 0, 0, 541, 542, 5, 5, 0, 0, 542, 543, 3, 90, 45, 0, 543, 544, 3, 68, 34, 0, 544, 89, 1, 0, 0, 0, 545, 546, 5, 63, 0, 0, 546, 547, 5, 56, 0, 0, 547, 548, 5, 63, 0, 0, 548, 549, 5, 26, 0, 0, 549, 556, 3, 114, 57, 0, 550, 551, 5, 63, 0, 0, 551, 552, 5, 26, 0, 0, 552, 556, 3, 114, 57, 0, 553, 556, 3, 114, 57, 0, 554, 556, 1, 0, 0, 0, 555, 545, 1, 0, 0, 0, 555, 550, 1, 0, 0, 0, 555, 553, 1, 0, 0, 0, 555, 554, 1, 0, 0, 0, 556, 91, 1, 0, 0, 0, 557, 558, 5, 6, 0, 0, 558, 93, 1, 0, 0, 0, 559, 560, 5, 7, 0, 0, 560, 95, 1, 0, 0, 0, 561, 563, 5, 4, 0, 0, 562, 564, 3, 114, 57, 0, 563, 562, 1, 0, 0, 0, 563, 564, 1, 0, 0, 0, 564, 97, 1, 0, 0, 0, 565, 567, 3, 102, 51, 0, 566, 568, 3, 100, 50, 0, 567, 566, 1, 0, 0, 0, 567, 568, 1, 0, 0, 0, 568, 572, 1, 0, 0, 0, 569, 572, 3, 110, 55, 0, 570, 572, 3, 112, 56, 0, 571, 565, 1, 0, 0, 0, 571, 569, 1, 0, 0, 0, 571, 570, 1, 0, 0, 0, 572, 99, 1, 0, 0, 0, 573, 574, 5, 63, 0, 0, 574, 101, 1, 0, 0, 0, 575, 578, 3, 104, 52, 0, 576, 578, 5, 23, 0, 0, 577, 575, 1, 0, 0, 0, 577, 576, 1, 0, 0, 0, 578, 103, 1, 0, 0, 0, 579, 582, 3, 106, 53, 0, 580, 582, 3, 108, 54, 0, 581, 579, 1, 0, 0, 0, 581, 580, 1, 0, 0, 0, 582, 105, 1, 0, 0, 0, 583, 584, 7, 2, 0, 0, 584, 107, 1, 0, 0, 0, 585, 586, 7, 3, 0, 0, 586, 109, 1, 0, 0, 0, 587, 588, 5, 11, 0, 0, 588, 590, 3, 102, 51, 0, 589, 591, 3, 100, 50, 0, 590, 589, 1, 0, 0, 0, 590, 591, 1, 0, 0, 0, 591, 595, 1, 0, 0, 0, 592, 593, 5, 11, 0, 0, 593, 595, 3, 112, 56, 0, 594, 587, 1, 0, 0, 0, 594, 592, 1, 0, 0, 0, 595, 111, 1, 0, 0, 0, 596, 597, 5, 24, 0, 0, 597, 599, 3, 102, 51, 0, 598, 600, 3, 100, 50, 0, 599, 598, 1, 0, 0, 0, 599, 600, 1, 0, 0, 0, 600, 113, 1, 0, 0, 0, 601, 602, 3, 116, 58, 0, 602, 115, 1, 0, 0, 0, 603, 608, 3, 118, 59, 0, 604, 605, 5, 48, 0, 0, 605, 607, 3, 118, 59, 0, 606, 604, 1, 0, 0, 0, 607, 610, 1, 0, 0, 0, 608, 606, 1, 0, 0, 0, 608, 609, 1, 0, 0, 0, 609, 117, 1, 0, 0, 0, 610, 608, 1, 0, 0, 0, 611, 616, 3, 120, 60, 0, 612, 613, 5, 47, 0, 0, 613, 615, 3, 120, 60, 0, 614, 612, 1, 0, 0, 0, 615, 618, 1, 0, 0, 0, 616, 614, 1, 0, 0, 0, 616, 617, 1, 0, 0, 0, 617, 119, 1, 0, 0, 0, 618, 616, 1, 0, 0, 0, 619, 624, 3, 122, 61, 0, 620, 621, 7, 4, 0, 0, 621, 623, 3, 122, 61, 0, 622, 620, 1, 0, 0, 0, 623, 626, 1, 0, 0, 0, 624, 622, 1, 0, 0, 0, 624, 625, 1, 0, 0, 0, 625, 121, 1, 0, 0, 0, 626, 624, 1, 0, 0, 0, 627, 632, 3, 124, 62, 0, 628, 629, 7, 5, 0, 0, 629, 631, 3, 124, 62, 0, 630, 628, 1, 0, 0, 0, 631, 634, 1, 0, 0, 0, 632, 630, 1, 0, 0, 0, 632, 633, 1, 0, 0, 0, 633, 123, 1, 0, 0, 0, 634, 632, 1, 0, 0, 0, 635, 640, 3, 126, 63, 0, 636, 637, 7, 6, 0, 0, 637, 639, 3, 126, 63, 0, 638, 636, 1, 0, 0, 0, 639, 642, 1, 0, 0, 0, 640, 638, 1, 0, 0, 0, 640, 641, 1, 0, 0, 0, 641, 125, 1, 0, 0, 0, 642, 640, 1, 0, 0, 0, 643, 648, 3, 128, 64, 0, 644, 645, 7, 7, 0, 0, 645, 647, 3, 128, 64, 0, 646, 644, 1, 0, 0, 0, 647, 650, 1, 0, 0, 0, 648, 646, 1, 0, 0, 0, 648, 649, 1, 0, 0, 0, 649, 127, 1, 0, 0, 0, 650, 648, 1, 0, 0, 0, 651, 654, 3, 130, 65, 0, 652, 653, 5, 40, 0, 0, 653, 655, 3, 128, 64, 0, 654, 652, 1, 0, 0, 0, 654, 655, 1, 0, 0, 0, 655, 129, 1, 0, 0, 0, 656, 657, 5, 36, 0, 0, 657, 662, 3, 130, 65, 0, 658, 659, 5, 49, 0, 0, 659, 662, 3, 130, 65, 0, 660, 662, 3, 132, 66, 0, 661, 656, 1, 0, 0, 0, 661, 658, 1, 0, 0, 0, 661, 660, 1, 0, 0, 0, 662, 131, 1, 0, 0, 0, 663, 668, 3, 138, 69, 0, 664, 667, 3, 134, 67, 0, 665, 667, 3, 136, 68, 0, 666, 664, 1, 0, 0, 0, 666, 665, 1, 0, 0, 0, 667, 670, 1, 0, 0, 0, 668, 666, 1, 0, 0, 0, 668, 669, 1, 0, 0, 0, 669, 133, 1, 0, 0, 0, 670, 668, 1, 0, 0, 0, 671, 672, 5, 54, 0, 0, 672, 673, 3, 114, 57, 0, 673, 674, 5, 55, 0, 0, 674, 685, 1, 0, 0, 0, 675, 677, 5, 54, 0, 0, 676, 678, 3, 114, 57, 0, 677, 676, 1, 0, 0, 0, 677, 678, 1, 0, 0, 0, 678, 679, 1, 0, 0, 0, 679, 681, 5, 57, 0, 0, 680, 682, 3, 114, 57, 0, 681, 680, 1, 0, 0, 0, 681, 682, 1, 0, 0, 0, 682, 683, 1, 0, 0, 0, 683, 685, 5, 55, 0, 0, 684, 671, 1, 0, 0, 0, 684, 675, 1, 0, 0, 0, 685, 135, 1, 0, 0, 0, 686, 688, 5, 50, 0, 0, 687, 689, 3, 66, 33, 0, 688, 687, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 690, 1, 0, 0, 0, 690, 691, 5, 51, 0, 0, 691, 137, 1, 0, 0, 0, 692, 701, 3, 142, 71, 0, 693, 701, 3, 54, 27, 0, 694, 701, 5, 63, 0, 0, 695, 696, 5, 50, 0, 0, 696, 697, 3, 114, 57, 0, 697, 698, 5, 51, 0, 0, 698, 701, 1, 0, 0, 0, 699, 701, 3, 140, 70, 0, 700, 692, 1, 0, 0, 0, 700, 693, 1, 0, 0, 0, 700, 694, 1, 0, 0, 0, 700, 695, 1, 0, 0, 0, 700, 699, 1, 0, 0, 0, 701, 139, 1, 0, 0, 0, 702, 703, 3, 98, 49, 0, 703, 704, 5, 50, 0, 0, 704, 705, 3, 114, 57, 0, 705, 706, 5, 51, 0, 0, 706, 141, 1, 0, 0, 0, 707, 712, 3, 144, 72, 0, 708, 712, 5, 61, 0, 0, 709, 712, 5, 62, 0, 0, 710, 712, 3, 146, 73, 0, 711, 707, 1, 0, 0, 0, 711, 708, 1, 0, 0, 0, 711, 709, 1, 0, 0, 0, 711, 710, 1, 0, 0, 0, 712, 143, 1, 0, 0, 0, 713, 715, 5, 36, 0, 0, 714, 713, 1, 0, 0, 0, 714, 715, 1, 0, 0, 0, 715, 716, 1, 0, 0, 0, 716, 719, 7, 8, 0, 0, 717, 718, 4, 72, 0, 0, 718, 720, 5, 63, 0, 0, 719, 717, 1, 0, 0, 0, 719, 720, 1, 0, 0, 0, 720, 145, 1, 0, 0, 0, 721, 723, 5, 54, 0, 0, 722, 724, 3, 148, 74, 0, 723, 722, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 55, 0, 0, 726, 147, 1, 0, 0, 0, 727, 732, 3, 114, 57, 0, 728, 729, 5, 56, 0, 0, 729, 731, 3, 114, 57, 0, 730, 728, 1, 0, 0, 0, 731, 734, 1, 0, 0, 0, 732, 730, 1, 0, 0, 0, 732, 733, 1, 0, 0, 0, 733, 736, 1, 0, 0, 0, 734, 732, 1, 0, 0, 0, 735, 737, 5, 56, 0, 0, 736, 735, 1, 0, 0, 0, 736, 737, 1, 0, 0, 0, 737, 149, 1, 0, 0, 0, 88, 153, 164, 173, 177, 182, 187, 191, 195, 204, 208, 214, 220, 228, 232, 241, 250, 254, 260, 264, 269, 274, 278, 280, 288, 292, 299, 304, 308, 310, 317, 321, 331, 335, 340, 344, 354, 358, 369, 374, 380, 389, 400, 412, 419, 423, 434, 438, 442, 451, 455, 461, 474, 478, 488, 498, 517, 527, 531, 555, 563, 567, 571, 577, 581, 590, 594, 599, 608, 616, 624, 632, 640, 648, 654, 661, 666, 668, 677, 681, 684, 688, 700, 711, 714, 719, 723, 732, 736] \ No newline at end of file +[4, 1, 66, 739, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 1, 0, 5, 0, 152, 8, 0, 10, 0, 12, 0, 155, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 165, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 172, 8, 2, 10, 2, 12, 2, 175, 9, 2, 1, 2, 3, 2, 178, 8, 2, 1, 3, 1, 3, 1, 3, 3, 3, 183, 8, 3, 1, 4, 1, 4, 1, 4, 3, 4, 188, 8, 4, 1, 4, 1, 4, 3, 4, 192, 8, 4, 1, 4, 1, 4, 3, 4, 196, 8, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 203, 8, 5, 10, 5, 12, 5, 206, 9, 5, 1, 5, 3, 5, 209, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 215, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 221, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 5, 8, 227, 8, 8, 10, 8, 12, 8, 230, 9, 8, 1, 8, 3, 8, 233, 8, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 3, 10, 242, 8, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 5, 11, 249, 8, 11, 10, 11, 12, 11, 252, 9, 11, 1, 11, 3, 11, 255, 8, 11, 1, 12, 1, 12, 1, 12, 1, 12, 3, 12, 261, 8, 12, 1, 13, 1, 13, 3, 13, 265, 8, 13, 1, 13, 1, 13, 1, 13, 3, 13, 270, 8, 13, 1, 13, 5, 13, 273, 8, 13, 10, 13, 12, 13, 276, 9, 13, 1, 13, 3, 13, 279, 8, 13, 3, 13, 281, 8, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 3, 14, 289, 8, 14, 1, 15, 1, 15, 3, 15, 293, 8, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 3, 16, 300, 8, 16, 1, 16, 5, 16, 303, 8, 16, 10, 16, 12, 16, 306, 9, 16, 1, 16, 3, 16, 309, 8, 16, 3, 16, 311, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 3, 17, 318, 8, 17, 1, 18, 1, 18, 3, 18, 322, 8, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 3, 19, 332, 8, 19, 1, 20, 1, 20, 3, 20, 336, 8, 20, 1, 20, 1, 20, 1, 20, 3, 20, 341, 8, 20, 4, 20, 343, 8, 20, 11, 20, 12, 20, 344, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 5, 22, 353, 8, 22, 10, 22, 12, 22, 356, 9, 22, 1, 22, 3, 22, 359, 8, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 5, 23, 368, 8, 23, 10, 23, 12, 23, 371, 9, 23, 1, 23, 1, 23, 3, 23, 375, 8, 23, 1, 24, 1, 24, 1, 24, 1, 24, 3, 24, 381, 8, 24, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 390, 8, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 401, 8, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 3, 28, 413, 8, 28, 1, 29, 1, 29, 1, 29, 5, 29, 418, 8, 29, 10, 29, 12, 29, 421, 9, 29, 1, 29, 3, 29, 424, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 5, 31, 433, 8, 31, 10, 31, 12, 31, 436, 9, 31, 1, 31, 3, 31, 439, 8, 31, 1, 32, 1, 32, 3, 32, 443, 8, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 5, 33, 450, 8, 33, 10, 33, 12, 33, 453, 9, 33, 1, 33, 3, 33, 456, 8, 33, 1, 34, 1, 34, 5, 34, 460, 8, 34, 10, 34, 12, 34, 463, 9, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 3, 35, 475, 8, 35, 1, 36, 1, 36, 3, 36, 479, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 489, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 499, 8, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 518, 8, 39, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 5, 41, 526, 8, 41, 10, 41, 12, 41, 529, 9, 41, 1, 41, 3, 41, 532, 8, 41, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 3, 45, 556, 8, 45, 1, 46, 1, 46, 1, 47, 1, 47, 1, 48, 1, 48, 3, 48, 564, 8, 48, 1, 49, 1, 49, 3, 49, 568, 8, 49, 1, 49, 1, 49, 3, 49, 572, 8, 49, 1, 50, 1, 50, 1, 51, 1, 51, 3, 51, 578, 8, 51, 1, 52, 1, 52, 3, 52, 582, 8, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 3, 55, 591, 8, 55, 1, 55, 1, 55, 3, 55, 595, 8, 55, 1, 56, 1, 56, 1, 56, 3, 56, 600, 8, 56, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 5, 58, 607, 8, 58, 10, 58, 12, 58, 610, 9, 58, 1, 59, 1, 59, 1, 59, 5, 59, 615, 8, 59, 10, 59, 12, 59, 618, 9, 59, 1, 60, 1, 60, 1, 60, 5, 60, 623, 8, 60, 10, 60, 12, 60, 626, 9, 60, 1, 61, 1, 61, 1, 61, 5, 61, 631, 8, 61, 10, 61, 12, 61, 634, 9, 61, 1, 62, 1, 62, 1, 62, 5, 62, 639, 8, 62, 10, 62, 12, 62, 642, 9, 62, 1, 63, 1, 63, 1, 63, 5, 63, 647, 8, 63, 10, 63, 12, 63, 650, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 655, 8, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 662, 8, 65, 1, 66, 1, 66, 1, 66, 5, 66, 667, 8, 66, 10, 66, 12, 66, 670, 9, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 3, 67, 678, 8, 67, 1, 67, 1, 67, 3, 67, 682, 8, 67, 1, 67, 3, 67, 685, 8, 67, 1, 68, 1, 68, 3, 68, 689, 8, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 701, 8, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 3, 71, 712, 8, 71, 1, 72, 3, 72, 715, 8, 72, 1, 72, 1, 72, 1, 72, 3, 72, 720, 8, 72, 1, 73, 1, 73, 3, 73, 724, 8, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 5, 74, 731, 8, 74, 10, 74, 12, 74, 734, 9, 74, 1, 74, 3, 74, 737, 8, 74, 1, 74, 0, 0, 75, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 0, 9, 2, 0, 25, 25, 28, 28, 1, 0, 30, 34, 1, 0, 13, 20, 1, 0, 21, 22, 1, 0, 41, 42, 1, 0, 43, 46, 1, 0, 35, 36, 1, 0, 37, 39, 1, 0, 59, 60, 780, 0, 153, 1, 0, 0, 0, 2, 164, 1, 0, 0, 0, 4, 177, 1, 0, 0, 0, 6, 182, 1, 0, 0, 0, 8, 184, 1, 0, 0, 0, 10, 199, 1, 0, 0, 0, 12, 210, 1, 0, 0, 0, 14, 220, 1, 0, 0, 0, 16, 222, 1, 0, 0, 0, 18, 236, 1, 0, 0, 0, 20, 239, 1, 0, 0, 0, 22, 245, 1, 0, 0, 0, 24, 256, 1, 0, 0, 0, 26, 262, 1, 0, 0, 0, 28, 288, 1, 0, 0, 0, 30, 290, 1, 0, 0, 0, 32, 296, 1, 0, 0, 0, 34, 317, 1, 0, 0, 0, 36, 321, 1, 0, 0, 0, 38, 331, 1, 0, 0, 0, 40, 335, 1, 0, 0, 0, 42, 346, 1, 0, 0, 0, 44, 348, 1, 0, 0, 0, 46, 362, 1, 0, 0, 0, 48, 380, 1, 0, 0, 0, 50, 382, 1, 0, 0, 0, 52, 389, 1, 0, 0, 0, 54, 400, 1, 0, 0, 0, 56, 412, 1, 0, 0, 0, 58, 414, 1, 0, 0, 0, 60, 425, 1, 0, 0, 0, 62, 429, 1, 0, 0, 0, 64, 440, 1, 0, 0, 0, 66, 446, 1, 0, 0, 0, 68, 457, 1, 0, 0, 0, 70, 474, 1, 0, 0, 0, 72, 478, 1, 0, 0, 0, 74, 488, 1, 0, 0, 0, 76, 498, 1, 0, 0, 0, 78, 517, 1, 0, 0, 0, 80, 519, 1, 0, 0, 0, 82, 521, 1, 0, 0, 0, 84, 533, 1, 0, 0, 0, 86, 538, 1, 0, 0, 0, 88, 541, 1, 0, 0, 0, 90, 555, 1, 0, 0, 0, 92, 557, 1, 0, 0, 0, 94, 559, 1, 0, 0, 0, 96, 561, 1, 0, 0, 0, 98, 571, 1, 0, 0, 0, 100, 573, 1, 0, 0, 0, 102, 577, 1, 0, 0, 0, 104, 581, 1, 0, 0, 0, 106, 583, 1, 0, 0, 0, 108, 585, 1, 0, 0, 0, 110, 594, 1, 0, 0, 0, 112, 596, 1, 0, 0, 0, 114, 601, 1, 0, 0, 0, 116, 603, 1, 0, 0, 0, 118, 611, 1, 0, 0, 0, 120, 619, 1, 0, 0, 0, 122, 627, 1, 0, 0, 0, 124, 635, 1, 0, 0, 0, 126, 643, 1, 0, 0, 0, 128, 651, 1, 0, 0, 0, 130, 661, 1, 0, 0, 0, 132, 663, 1, 0, 0, 0, 134, 684, 1, 0, 0, 0, 136, 686, 1, 0, 0, 0, 138, 700, 1, 0, 0, 0, 140, 702, 1, 0, 0, 0, 142, 711, 1, 0, 0, 0, 144, 714, 1, 0, 0, 0, 146, 721, 1, 0, 0, 0, 148, 727, 1, 0, 0, 0, 150, 152, 3, 2, 1, 0, 151, 150, 1, 0, 0, 0, 152, 155, 1, 0, 0, 0, 153, 151, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 156, 1, 0, 0, 0, 155, 153, 1, 0, 0, 0, 156, 157, 5, 0, 0, 1, 157, 1, 1, 0, 0, 0, 158, 165, 3, 4, 2, 0, 159, 165, 3, 8, 4, 0, 160, 165, 3, 40, 20, 0, 161, 165, 3, 26, 13, 0, 162, 165, 3, 30, 15, 0, 163, 165, 3, 38, 19, 0, 164, 158, 1, 0, 0, 0, 164, 159, 1, 0, 0, 0, 164, 160, 1, 0, 0, 0, 164, 161, 1, 0, 0, 0, 164, 162, 1, 0, 0, 0, 164, 163, 1, 0, 0, 0, 165, 3, 1, 0, 0, 0, 166, 167, 5, 12, 0, 0, 167, 178, 5, 59, 0, 0, 168, 169, 5, 12, 0, 0, 169, 173, 5, 50, 0, 0, 170, 172, 3, 6, 3, 0, 171, 170, 1, 0, 0, 0, 172, 175, 1, 0, 0, 0, 173, 171, 1, 0, 0, 0, 173, 174, 1, 0, 0, 0, 174, 176, 1, 0, 0, 0, 175, 173, 1, 0, 0, 0, 176, 178, 5, 51, 0, 0, 177, 166, 1, 0, 0, 0, 177, 168, 1, 0, 0, 0, 178, 5, 1, 0, 0, 0, 179, 183, 5, 59, 0, 0, 180, 181, 5, 63, 0, 0, 181, 183, 5, 59, 0, 0, 182, 179, 1, 0, 0, 0, 182, 180, 1, 0, 0, 0, 183, 7, 1, 0, 0, 0, 184, 185, 5, 1, 0, 0, 185, 187, 5, 63, 0, 0, 186, 188, 3, 20, 10, 0, 187, 186, 1, 0, 0, 0, 187, 188, 1, 0, 0, 0, 188, 189, 1, 0, 0, 0, 189, 191, 5, 50, 0, 0, 190, 192, 3, 10, 5, 0, 191, 190, 1, 0, 0, 0, 191, 192, 1, 0, 0, 0, 192, 193, 1, 0, 0, 0, 193, 195, 5, 51, 0, 0, 194, 196, 3, 14, 7, 0, 195, 194, 1, 0, 0, 0, 195, 196, 1, 0, 0, 0, 196, 197, 1, 0, 0, 0, 197, 198, 3, 68, 34, 0, 198, 9, 1, 0, 0, 0, 199, 204, 3, 12, 6, 0, 200, 201, 5, 56, 0, 0, 201, 203, 3, 12, 6, 0, 202, 200, 1, 0, 0, 0, 203, 206, 1, 0, 0, 0, 204, 202, 1, 0, 0, 0, 204, 205, 1, 0, 0, 0, 205, 208, 1, 0, 0, 0, 206, 204, 1, 0, 0, 0, 207, 209, 5, 56, 0, 0, 208, 207, 1, 0, 0, 0, 208, 209, 1, 0, 0, 0, 209, 11, 1, 0, 0, 0, 210, 211, 5, 63, 0, 0, 211, 214, 3, 98, 49, 0, 212, 213, 5, 29, 0, 0, 213, 215, 3, 142, 71, 0, 214, 212, 1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 13, 1, 0, 0, 0, 216, 221, 3, 98, 49, 0, 217, 218, 5, 63, 0, 0, 218, 221, 3, 98, 49, 0, 219, 221, 3, 16, 8, 0, 220, 216, 1, 0, 0, 0, 220, 217, 1, 0, 0, 0, 220, 219, 1, 0, 0, 0, 221, 15, 1, 0, 0, 0, 222, 223, 5, 50, 0, 0, 223, 228, 3, 18, 9, 0, 224, 225, 5, 56, 0, 0, 225, 227, 3, 18, 9, 0, 226, 224, 1, 0, 0, 0, 227, 230, 1, 0, 0, 0, 228, 226, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 232, 1, 0, 0, 0, 230, 228, 1, 0, 0, 0, 231, 233, 5, 56, 0, 0, 232, 231, 1, 0, 0, 0, 232, 233, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 5, 51, 0, 0, 235, 17, 1, 0, 0, 0, 236, 237, 5, 63, 0, 0, 237, 238, 3, 98, 49, 0, 238, 19, 1, 0, 0, 0, 239, 241, 5, 52, 0, 0, 240, 242, 3, 22, 11, 0, 241, 240, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 1, 0, 0, 0, 243, 244, 5, 53, 0, 0, 244, 21, 1, 0, 0, 0, 245, 250, 3, 24, 12, 0, 246, 247, 5, 56, 0, 0, 247, 249, 3, 24, 12, 0, 248, 246, 1, 0, 0, 0, 249, 252, 1, 0, 0, 0, 250, 248, 1, 0, 0, 0, 250, 251, 1, 0, 0, 0, 251, 254, 1, 0, 0, 0, 252, 250, 1, 0, 0, 0, 253, 255, 5, 56, 0, 0, 254, 253, 1, 0, 0, 0, 254, 255, 1, 0, 0, 0, 255, 23, 1, 0, 0, 0, 256, 257, 5, 63, 0, 0, 257, 260, 3, 98, 49, 0, 258, 259, 5, 29, 0, 0, 259, 261, 3, 142, 71, 0, 260, 258, 1, 0, 0, 0, 260, 261, 1, 0, 0, 0, 261, 25, 1, 0, 0, 0, 262, 264, 5, 8, 0, 0, 263, 265, 5, 63, 0, 0, 264, 263, 1, 0, 0, 0, 264, 265, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 280, 5, 52, 0, 0, 267, 274, 3, 28, 14, 0, 268, 270, 5, 56, 0, 0, 269, 268, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273, 3, 28, 14, 0, 272, 269, 1, 0, 0, 0, 273, 276, 1, 0, 0, 0, 274, 272, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 278, 1, 0, 0, 0, 276, 274, 1, 0, 0, 0, 277, 279, 5, 56, 0, 0, 278, 277, 1, 0, 0, 0, 278, 279, 1, 0, 0, 0, 279, 281, 1, 0, 0, 0, 280, 267, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 283, 5, 53, 0, 0, 283, 27, 1, 0, 0, 0, 284, 289, 3, 30, 15, 0, 285, 289, 3, 26, 13, 0, 286, 289, 3, 40, 20, 0, 287, 289, 3, 36, 18, 0, 288, 284, 1, 0, 0, 0, 288, 285, 1, 0, 0, 0, 288, 286, 1, 0, 0, 0, 288, 287, 1, 0, 0, 0, 289, 29, 1, 0, 0, 0, 290, 292, 5, 9, 0, 0, 291, 293, 5, 63, 0, 0, 292, 291, 1, 0, 0, 0, 292, 293, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 3, 32, 16, 0, 295, 31, 1, 0, 0, 0, 296, 310, 5, 52, 0, 0, 297, 304, 3, 34, 17, 0, 298, 300, 5, 56, 0, 0, 299, 298, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 303, 3, 34, 17, 0, 302, 299, 1, 0, 0, 0, 303, 306, 1, 0, 0, 0, 304, 302, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 308, 1, 0, 0, 0, 306, 304, 1, 0, 0, 0, 307, 309, 5, 56, 0, 0, 308, 307, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 297, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 1, 0, 0, 0, 312, 313, 5, 53, 0, 0, 313, 33, 1, 0, 0, 0, 314, 318, 3, 40, 20, 0, 315, 318, 3, 36, 18, 0, 316, 318, 3, 26, 13, 0, 317, 314, 1, 0, 0, 0, 317, 315, 1, 0, 0, 0, 317, 316, 1, 0, 0, 0, 318, 35, 1, 0, 0, 0, 319, 322, 3, 52, 26, 0, 320, 322, 3, 114, 57, 0, 321, 319, 1, 0, 0, 0, 321, 320, 1, 0, 0, 0, 322, 37, 1, 0, 0, 0, 323, 324, 5, 63, 0, 0, 324, 325, 5, 26, 0, 0, 325, 332, 3, 142, 71, 0, 326, 327, 5, 63, 0, 0, 327, 328, 3, 98, 49, 0, 328, 329, 5, 26, 0, 0, 329, 330, 3, 142, 71, 0, 330, 332, 1, 0, 0, 0, 331, 323, 1, 0, 0, 0, 331, 326, 1, 0, 0, 0, 332, 39, 1, 0, 0, 0, 333, 336, 3, 44, 22, 0, 334, 336, 3, 48, 24, 0, 335, 333, 1, 0, 0, 0, 335, 334, 1, 0, 0, 0, 336, 342, 1, 0, 0, 0, 337, 340, 3, 42, 21, 0, 338, 341, 3, 44, 22, 0, 339, 341, 3, 48, 24, 0, 340, 338, 1, 0, 0, 0, 340, 339, 1, 0, 0, 0, 341, 343, 1, 0, 0, 0, 342, 337, 1, 0, 0, 0, 343, 344, 1, 0, 0, 0, 344, 342, 1, 0, 0, 0, 344, 345, 1, 0, 0, 0, 345, 41, 1, 0, 0, 0, 346, 347, 7, 0, 0, 0, 347, 43, 1, 0, 0, 0, 348, 349, 5, 52, 0, 0, 349, 354, 3, 46, 23, 0, 350, 351, 5, 56, 0, 0, 351, 353, 3, 46, 23, 0, 352, 350, 1, 0, 0, 0, 353, 356, 1, 0, 0, 0, 354, 352, 1, 0, 0, 0, 354, 355, 1, 0, 0, 0, 355, 358, 1, 0, 0, 0, 356, 354, 1, 0, 0, 0, 357, 359, 5, 56, 0, 0, 358, 357, 1, 0, 0, 0, 358, 359, 1, 0, 0, 0, 359, 360, 1, 0, 0, 0, 360, 361, 5, 53, 0, 0, 361, 45, 1, 0, 0, 0, 362, 363, 5, 63, 0, 0, 363, 364, 5, 57, 0, 0, 364, 369, 3, 48, 24, 0, 365, 366, 5, 25, 0, 0, 366, 368, 3, 48, 24, 0, 367, 365, 1, 0, 0, 0, 368, 371, 1, 0, 0, 0, 369, 367, 1, 0, 0, 0, 369, 370, 1, 0, 0, 0, 370, 374, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 372, 373, 5, 57, 0, 0, 373, 375, 5, 63, 0, 0, 374, 372, 1, 0, 0, 0, 374, 375, 1, 0, 0, 0, 375, 47, 1, 0, 0, 0, 376, 381, 3, 50, 25, 0, 377, 381, 3, 52, 26, 0, 378, 381, 3, 114, 57, 0, 379, 381, 5, 10, 0, 0, 380, 376, 1, 0, 0, 0, 380, 377, 1, 0, 0, 0, 380, 378, 1, 0, 0, 0, 380, 379, 1, 0, 0, 0, 381, 49, 1, 0, 0, 0, 382, 383, 5, 63, 0, 0, 383, 51, 1, 0, 0, 0, 384, 385, 3, 54, 27, 0, 385, 386, 3, 56, 28, 0, 386, 390, 1, 0, 0, 0, 387, 388, 5, 63, 0, 0, 388, 390, 3, 56, 28, 0, 389, 384, 1, 0, 0, 0, 389, 387, 1, 0, 0, 0, 390, 53, 1, 0, 0, 0, 391, 392, 5, 63, 0, 0, 392, 393, 5, 58, 0, 0, 393, 401, 5, 63, 0, 0, 394, 395, 5, 63, 0, 0, 395, 396, 5, 58, 0, 0, 396, 401, 5, 5, 0, 0, 397, 398, 5, 12, 0, 0, 398, 399, 5, 58, 0, 0, 399, 401, 5, 63, 0, 0, 400, 391, 1, 0, 0, 0, 400, 394, 1, 0, 0, 0, 400, 397, 1, 0, 0, 0, 401, 55, 1, 0, 0, 0, 402, 403, 5, 52, 0, 0, 403, 413, 5, 53, 0, 0, 404, 405, 5, 52, 0, 0, 405, 406, 3, 58, 29, 0, 406, 407, 5, 53, 0, 0, 407, 413, 1, 0, 0, 0, 408, 409, 5, 52, 0, 0, 409, 410, 3, 62, 31, 0, 410, 411, 5, 53, 0, 0, 411, 413, 1, 0, 0, 0, 412, 402, 1, 0, 0, 0, 412, 404, 1, 0, 0, 0, 412, 408, 1, 0, 0, 0, 413, 57, 1, 0, 0, 0, 414, 419, 3, 60, 30, 0, 415, 416, 5, 56, 0, 0, 416, 418, 3, 60, 30, 0, 417, 415, 1, 0, 0, 0, 418, 421, 1, 0, 0, 0, 419, 417, 1, 0, 0, 0, 419, 420, 1, 0, 0, 0, 420, 423, 1, 0, 0, 0, 421, 419, 1, 0, 0, 0, 422, 424, 5, 56, 0, 0, 423, 422, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 59, 1, 0, 0, 0, 425, 426, 5, 63, 0, 0, 426, 427, 5, 29, 0, 0, 427, 428, 3, 114, 57, 0, 428, 61, 1, 0, 0, 0, 429, 434, 3, 114, 57, 0, 430, 431, 5, 56, 0, 0, 431, 433, 3, 114, 57, 0, 432, 430, 1, 0, 0, 0, 433, 436, 1, 0, 0, 0, 434, 432, 1, 0, 0, 0, 434, 435, 1, 0, 0, 0, 435, 438, 1, 0, 0, 0, 436, 434, 1, 0, 0, 0, 437, 439, 5, 56, 0, 0, 438, 437, 1, 0, 0, 0, 438, 439, 1, 0, 0, 0, 439, 63, 1, 0, 0, 0, 440, 442, 5, 50, 0, 0, 441, 443, 3, 66, 33, 0, 442, 441, 1, 0, 0, 0, 442, 443, 1, 0, 0, 0, 443, 444, 1, 0, 0, 0, 444, 445, 5, 51, 0, 0, 445, 65, 1, 0, 0, 0, 446, 451, 3, 114, 57, 0, 447, 448, 5, 56, 0, 0, 448, 450, 3, 114, 57, 0, 449, 447, 1, 0, 0, 0, 450, 453, 1, 0, 0, 0, 451, 449, 1, 0, 0, 0, 451, 452, 1, 0, 0, 0, 452, 455, 1, 0, 0, 0, 453, 451, 1, 0, 0, 0, 454, 456, 5, 56, 0, 0, 455, 454, 1, 0, 0, 0, 455, 456, 1, 0, 0, 0, 456, 67, 1, 0, 0, 0, 457, 461, 5, 52, 0, 0, 458, 460, 3, 70, 35, 0, 459, 458, 1, 0, 0, 0, 460, 463, 1, 0, 0, 0, 461, 459, 1, 0, 0, 0, 461, 462, 1, 0, 0, 0, 462, 464, 1, 0, 0, 0, 463, 461, 1, 0, 0, 0, 464, 465, 5, 53, 0, 0, 465, 69, 1, 0, 0, 0, 466, 475, 3, 72, 36, 0, 467, 475, 3, 78, 39, 0, 468, 475, 3, 82, 41, 0, 469, 475, 3, 88, 44, 0, 470, 475, 3, 92, 46, 0, 471, 475, 3, 94, 47, 0, 472, 475, 3, 96, 48, 0, 473, 475, 3, 114, 57, 0, 474, 466, 1, 0, 0, 0, 474, 467, 1, 0, 0, 0, 474, 468, 1, 0, 0, 0, 474, 469, 1, 0, 0, 0, 474, 470, 1, 0, 0, 0, 474, 471, 1, 0, 0, 0, 474, 472, 1, 0, 0, 0, 474, 473, 1, 0, 0, 0, 475, 71, 1, 0, 0, 0, 476, 479, 3, 74, 37, 0, 477, 479, 3, 76, 38, 0, 478, 476, 1, 0, 0, 0, 478, 477, 1, 0, 0, 0, 479, 73, 1, 0, 0, 0, 480, 481, 5, 63, 0, 0, 481, 482, 5, 26, 0, 0, 482, 489, 3, 114, 57, 0, 483, 484, 5, 63, 0, 0, 484, 485, 3, 98, 49, 0, 485, 486, 5, 26, 0, 0, 486, 487, 3, 114, 57, 0, 487, 489, 1, 0, 0, 0, 488, 480, 1, 0, 0, 0, 488, 483, 1, 0, 0, 0, 489, 75, 1, 0, 0, 0, 490, 491, 5, 63, 0, 0, 491, 492, 5, 27, 0, 0, 492, 499, 3, 114, 57, 0, 493, 494, 5, 63, 0, 0, 494, 495, 3, 98, 49, 0, 495, 496, 5, 27, 0, 0, 496, 497, 3, 114, 57, 0, 497, 499, 1, 0, 0, 0, 498, 490, 1, 0, 0, 0, 498, 493, 1, 0, 0, 0, 499, 77, 1, 0, 0, 0, 500, 501, 5, 63, 0, 0, 501, 502, 5, 29, 0, 0, 502, 518, 3, 114, 57, 0, 503, 504, 5, 63, 0, 0, 504, 505, 3, 134, 67, 0, 505, 506, 5, 29, 0, 0, 506, 507, 3, 114, 57, 0, 507, 518, 1, 0, 0, 0, 508, 509, 5, 63, 0, 0, 509, 510, 3, 80, 40, 0, 510, 511, 3, 114, 57, 0, 511, 518, 1, 0, 0, 0, 512, 513, 5, 63, 0, 0, 513, 514, 3, 134, 67, 0, 514, 515, 3, 80, 40, 0, 515, 516, 3, 114, 57, 0, 516, 518, 1, 0, 0, 0, 517, 500, 1, 0, 0, 0, 517, 503, 1, 0, 0, 0, 517, 508, 1, 0, 0, 0, 517, 512, 1, 0, 0, 0, 518, 79, 1, 0, 0, 0, 519, 520, 7, 1, 0, 0, 520, 81, 1, 0, 0, 0, 521, 522, 5, 2, 0, 0, 522, 523, 3, 114, 57, 0, 523, 527, 3, 68, 34, 0, 524, 526, 3, 84, 42, 0, 525, 524, 1, 0, 0, 0, 526, 529, 1, 0, 0, 0, 527, 525, 1, 0, 0, 0, 527, 528, 1, 0, 0, 0, 528, 531, 1, 0, 0, 0, 529, 527, 1, 0, 0, 0, 530, 532, 3, 86, 43, 0, 531, 530, 1, 0, 0, 0, 531, 532, 1, 0, 0, 0, 532, 83, 1, 0, 0, 0, 533, 534, 5, 3, 0, 0, 534, 535, 5, 2, 0, 0, 535, 536, 3, 114, 57, 0, 536, 537, 3, 68, 34, 0, 537, 85, 1, 0, 0, 0, 538, 539, 5, 3, 0, 0, 539, 540, 3, 68, 34, 0, 540, 87, 1, 0, 0, 0, 541, 542, 5, 5, 0, 0, 542, 543, 3, 90, 45, 0, 543, 544, 3, 68, 34, 0, 544, 89, 1, 0, 0, 0, 545, 546, 5, 63, 0, 0, 546, 547, 5, 56, 0, 0, 547, 548, 5, 63, 0, 0, 548, 549, 5, 26, 0, 0, 549, 556, 3, 114, 57, 0, 550, 551, 5, 63, 0, 0, 551, 552, 5, 26, 0, 0, 552, 556, 3, 114, 57, 0, 553, 556, 3, 114, 57, 0, 554, 556, 1, 0, 0, 0, 555, 545, 1, 0, 0, 0, 555, 550, 1, 0, 0, 0, 555, 553, 1, 0, 0, 0, 555, 554, 1, 0, 0, 0, 556, 91, 1, 0, 0, 0, 557, 558, 5, 6, 0, 0, 558, 93, 1, 0, 0, 0, 559, 560, 5, 7, 0, 0, 560, 95, 1, 0, 0, 0, 561, 563, 5, 4, 0, 0, 562, 564, 3, 114, 57, 0, 563, 562, 1, 0, 0, 0, 563, 564, 1, 0, 0, 0, 564, 97, 1, 0, 0, 0, 565, 567, 3, 102, 51, 0, 566, 568, 3, 100, 50, 0, 567, 566, 1, 0, 0, 0, 567, 568, 1, 0, 0, 0, 568, 572, 1, 0, 0, 0, 569, 572, 3, 110, 55, 0, 570, 572, 3, 112, 56, 0, 571, 565, 1, 0, 0, 0, 571, 569, 1, 0, 0, 0, 571, 570, 1, 0, 0, 0, 572, 99, 1, 0, 0, 0, 573, 574, 5, 63, 0, 0, 574, 101, 1, 0, 0, 0, 575, 578, 3, 104, 52, 0, 576, 578, 5, 23, 0, 0, 577, 575, 1, 0, 0, 0, 577, 576, 1, 0, 0, 0, 578, 103, 1, 0, 0, 0, 579, 582, 3, 106, 53, 0, 580, 582, 3, 108, 54, 0, 581, 579, 1, 0, 0, 0, 581, 580, 1, 0, 0, 0, 582, 105, 1, 0, 0, 0, 583, 584, 7, 2, 0, 0, 584, 107, 1, 0, 0, 0, 585, 586, 7, 3, 0, 0, 586, 109, 1, 0, 0, 0, 587, 588, 5, 11, 0, 0, 588, 590, 3, 102, 51, 0, 589, 591, 3, 100, 50, 0, 590, 589, 1, 0, 0, 0, 590, 591, 1, 0, 0, 0, 591, 595, 1, 0, 0, 0, 592, 593, 5, 11, 0, 0, 593, 595, 3, 112, 56, 0, 594, 587, 1, 0, 0, 0, 594, 592, 1, 0, 0, 0, 595, 111, 1, 0, 0, 0, 596, 597, 5, 24, 0, 0, 597, 599, 3, 102, 51, 0, 598, 600, 3, 100, 50, 0, 599, 598, 1, 0, 0, 0, 599, 600, 1, 0, 0, 0, 600, 113, 1, 0, 0, 0, 601, 602, 3, 116, 58, 0, 602, 115, 1, 0, 0, 0, 603, 608, 3, 118, 59, 0, 604, 605, 5, 48, 0, 0, 605, 607, 3, 118, 59, 0, 606, 604, 1, 0, 0, 0, 607, 610, 1, 0, 0, 0, 608, 606, 1, 0, 0, 0, 608, 609, 1, 0, 0, 0, 609, 117, 1, 0, 0, 0, 610, 608, 1, 0, 0, 0, 611, 616, 3, 120, 60, 0, 612, 613, 5, 47, 0, 0, 613, 615, 3, 120, 60, 0, 614, 612, 1, 0, 0, 0, 615, 618, 1, 0, 0, 0, 616, 614, 1, 0, 0, 0, 616, 617, 1, 0, 0, 0, 617, 119, 1, 0, 0, 0, 618, 616, 1, 0, 0, 0, 619, 624, 3, 122, 61, 0, 620, 621, 7, 4, 0, 0, 621, 623, 3, 122, 61, 0, 622, 620, 1, 0, 0, 0, 623, 626, 1, 0, 0, 0, 624, 622, 1, 0, 0, 0, 624, 625, 1, 0, 0, 0, 625, 121, 1, 0, 0, 0, 626, 624, 1, 0, 0, 0, 627, 632, 3, 124, 62, 0, 628, 629, 7, 5, 0, 0, 629, 631, 3, 124, 62, 0, 630, 628, 1, 0, 0, 0, 631, 634, 1, 0, 0, 0, 632, 630, 1, 0, 0, 0, 632, 633, 1, 0, 0, 0, 633, 123, 1, 0, 0, 0, 634, 632, 1, 0, 0, 0, 635, 640, 3, 126, 63, 0, 636, 637, 7, 6, 0, 0, 637, 639, 3, 126, 63, 0, 638, 636, 1, 0, 0, 0, 639, 642, 1, 0, 0, 0, 640, 638, 1, 0, 0, 0, 640, 641, 1, 0, 0, 0, 641, 125, 1, 0, 0, 0, 642, 640, 1, 0, 0, 0, 643, 648, 3, 128, 64, 0, 644, 645, 7, 7, 0, 0, 645, 647, 3, 128, 64, 0, 646, 644, 1, 0, 0, 0, 647, 650, 1, 0, 0, 0, 648, 646, 1, 0, 0, 0, 648, 649, 1, 0, 0, 0, 649, 127, 1, 0, 0, 0, 650, 648, 1, 0, 0, 0, 651, 654, 3, 130, 65, 0, 652, 653, 5, 40, 0, 0, 653, 655, 3, 128, 64, 0, 654, 652, 1, 0, 0, 0, 654, 655, 1, 0, 0, 0, 655, 129, 1, 0, 0, 0, 656, 657, 5, 36, 0, 0, 657, 662, 3, 130, 65, 0, 658, 659, 5, 49, 0, 0, 659, 662, 3, 130, 65, 0, 660, 662, 3, 132, 66, 0, 661, 656, 1, 0, 0, 0, 661, 658, 1, 0, 0, 0, 661, 660, 1, 0, 0, 0, 662, 131, 1, 0, 0, 0, 663, 668, 3, 138, 69, 0, 664, 667, 3, 134, 67, 0, 665, 667, 3, 136, 68, 0, 666, 664, 1, 0, 0, 0, 666, 665, 1, 0, 0, 0, 667, 670, 1, 0, 0, 0, 668, 666, 1, 0, 0, 0, 668, 669, 1, 0, 0, 0, 669, 133, 1, 0, 0, 0, 670, 668, 1, 0, 0, 0, 671, 672, 5, 54, 0, 0, 672, 673, 3, 114, 57, 0, 673, 674, 5, 55, 0, 0, 674, 685, 1, 0, 0, 0, 675, 677, 5, 54, 0, 0, 676, 678, 3, 114, 57, 0, 677, 676, 1, 0, 0, 0, 677, 678, 1, 0, 0, 0, 678, 679, 1, 0, 0, 0, 679, 681, 5, 57, 0, 0, 680, 682, 3, 114, 57, 0, 681, 680, 1, 0, 0, 0, 681, 682, 1, 0, 0, 0, 682, 683, 1, 0, 0, 0, 683, 685, 5, 55, 0, 0, 684, 671, 1, 0, 0, 0, 684, 675, 1, 0, 0, 0, 685, 135, 1, 0, 0, 0, 686, 688, 5, 50, 0, 0, 687, 689, 3, 66, 33, 0, 688, 687, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 690, 1, 0, 0, 0, 690, 691, 5, 51, 0, 0, 691, 137, 1, 0, 0, 0, 692, 701, 3, 142, 71, 0, 693, 701, 3, 54, 27, 0, 694, 701, 5, 63, 0, 0, 695, 696, 5, 50, 0, 0, 696, 697, 3, 114, 57, 0, 697, 698, 5, 51, 0, 0, 698, 701, 1, 0, 0, 0, 699, 701, 3, 140, 70, 0, 700, 692, 1, 0, 0, 0, 700, 693, 1, 0, 0, 0, 700, 694, 1, 0, 0, 0, 700, 695, 1, 0, 0, 0, 700, 699, 1, 0, 0, 0, 701, 139, 1, 0, 0, 0, 702, 703, 3, 98, 49, 0, 703, 704, 5, 50, 0, 0, 704, 705, 3, 114, 57, 0, 705, 706, 5, 51, 0, 0, 706, 141, 1, 0, 0, 0, 707, 712, 3, 144, 72, 0, 708, 712, 5, 62, 0, 0, 709, 712, 5, 61, 0, 0, 710, 712, 3, 146, 73, 0, 711, 707, 1, 0, 0, 0, 711, 708, 1, 0, 0, 0, 711, 709, 1, 0, 0, 0, 711, 710, 1, 0, 0, 0, 712, 143, 1, 0, 0, 0, 713, 715, 5, 36, 0, 0, 714, 713, 1, 0, 0, 0, 714, 715, 1, 0, 0, 0, 715, 716, 1, 0, 0, 0, 716, 719, 7, 8, 0, 0, 717, 718, 4, 72, 0, 0, 718, 720, 5, 63, 0, 0, 719, 717, 1, 0, 0, 0, 719, 720, 1, 0, 0, 0, 720, 145, 1, 0, 0, 0, 721, 723, 5, 54, 0, 0, 722, 724, 3, 148, 74, 0, 723, 722, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 55, 0, 0, 726, 147, 1, 0, 0, 0, 727, 732, 3, 114, 57, 0, 728, 729, 5, 56, 0, 0, 729, 731, 3, 114, 57, 0, 730, 728, 1, 0, 0, 0, 731, 734, 1, 0, 0, 0, 732, 730, 1, 0, 0, 0, 732, 733, 1, 0, 0, 0, 733, 736, 1, 0, 0, 0, 734, 732, 1, 0, 0, 0, 735, 737, 5, 56, 0, 0, 736, 735, 1, 0, 0, 0, 736, 737, 1, 0, 0, 0, 737, 149, 1, 0, 0, 0, 88, 153, 164, 173, 177, 182, 187, 191, 195, 204, 208, 214, 220, 228, 232, 241, 250, 254, 260, 264, 269, 274, 278, 280, 288, 292, 299, 304, 308, 310, 317, 321, 331, 335, 340, 344, 354, 358, 369, 374, 380, 389, 400, 412, 419, 423, 434, 438, 442, 451, 455, 461, 474, 478, 488, 498, 517, 527, 531, 555, 563, 567, 571, 577, 581, 590, 594, 599, 608, 616, 624, 632, 640, 648, 654, 661, 666, 668, 677, 681, 684, 688, 700, 711, 714, 719, 723, 732, 736] \ No newline at end of file diff --git a/arc/go/parser/ArcParser.tokens b/arc/go/parser/ArcParser.tokens index 692915c751..201c4aec50 100644 --- a/arc/go/parser/ArcParser.tokens +++ b/arc/go/parser/ArcParser.tokens @@ -58,8 +58,8 @@ COLON=57 DOT=58 INTEGER_LITERAL=59 FLOAT_LITERAL=60 -STR_LITERAL=61 -STR_LITERAL_RAW=62 +STR_LITERAL_MULTI=61 +STR_LITERAL=62 IDENTIFIER=63 SINGLE_LINE_COMMENT=64 MULTI_LINE_COMMENT=65 diff --git a/arc/go/parser/arc_lexer.go b/arc/go/parser/arc_lexer.go index 8142a996fa..a8ed684a49 100644 --- a/arc/go/parser/arc_lexer.go +++ b/arc/go/parser/arc_lexer.go @@ -71,7 +71,7 @@ func arclexerLexerInit() { "SLASH", "PERCENT", "CARET", "EQ", "NEQ", "LT", "GT", "LEQ", "GEQ", "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", "COLON", "DOT", "INTEGER_LITERAL", "FLOAT_LITERAL", - "STR_LITERAL", "STR_LITERAL_RAW", "IDENTIFIER", "SINGLE_LINE_COMMENT", + "STR_LITERAL_MULTI", "STR_LITERAL", "IDENTIFIER", "SINGLE_LINE_COMMENT", "MULTI_LINE_COMMENT", "WS", } staticData.RuleNames = []string{ @@ -83,12 +83,12 @@ func arclexerLexerInit() { "SLASH", "PERCENT", "CARET", "EQ", "NEQ", "LT", "GT", "LEQ", "GEQ", "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", "COLON", "DOT", "DIGITS", "DIGIT", "INTEGER_LITERAL", - "FLOAT_LITERAL", "STR_LITERAL", "STR_LITERAL_RAW", "ESCAPE_SEQUENCE", - "IDENTIFIER", "SINGLE_LINE_COMMENT", "MULTI_LINE_COMMENT", "WS", + "FLOAT_LITERAL", "STR_LITERAL_MULTI", "STR_LITERAL", "STR_PREFIX", "IDENTIFIER", + "SINGLE_LINE_COMMENT", "MULTI_LINE_COMMENT", "WS", } staticData.PredictionContextCache = antlr.NewPredictionContextCache() staticData.serializedATN = []int32{ - 4, 0, 66, 437, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, + 4, 0, 66, 440, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, @@ -122,163 +122,164 @@ func arclexerLexerInit() { 1, 50, 1, 51, 1, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 4, 58, 350, 8, 58, 11, 58, 12, 58, 351, 1, 59, 1, 59, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 3, 61, 361, 8, 61, - 1, 61, 1, 61, 3, 61, 365, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 370, 8, 62, - 10, 62, 12, 62, 373, 9, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 5, - 63, 381, 8, 63, 10, 63, 12, 63, 384, 9, 63, 1, 63, 1, 63, 1, 64, 1, 64, - 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 3, 64, 397, 8, 64, 1, - 65, 1, 65, 5, 65, 401, 8, 65, 10, 65, 12, 65, 404, 9, 65, 1, 66, 1, 66, - 1, 66, 1, 66, 5, 66, 410, 8, 66, 10, 66, 12, 66, 413, 9, 66, 1, 66, 1, - 66, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 421, 8, 67, 10, 67, 12, 67, 424, - 9, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 4, 68, 432, 8, 68, 11, - 68, 12, 68, 433, 1, 68, 1, 68, 1, 422, 0, 69, 1, 1, 3, 2, 5, 3, 7, 4, 9, - 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, - 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, - 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, - 65, 33, 67, 34, 69, 35, 71, 36, 73, 37, 75, 38, 77, 39, 79, 40, 81, 41, - 83, 42, 85, 43, 87, 44, 89, 45, 91, 46, 93, 47, 95, 48, 97, 49, 99, 50, - 101, 51, 103, 52, 105, 53, 107, 54, 109, 55, 111, 56, 113, 57, 115, 58, - 117, 0, 119, 0, 121, 59, 123, 60, 125, 61, 127, 62, 129, 0, 131, 63, 133, - 64, 135, 65, 137, 66, 1, 0, 9, 1, 0, 48, 57, 4, 0, 10, 10, 13, 13, 34, - 34, 92, 92, 1, 0, 96, 96, 7, 0, 34, 34, 92, 92, 98, 98, 102, 102, 110, - 110, 114, 114, 116, 116, 3, 0, 48, 57, 65, 70, 97, 102, 3, 0, 65, 90, 95, - 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 10, 10, 13, 13, - 3, 0, 9, 10, 13, 13, 32, 32, 445, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, - 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, - 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, - 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, - 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, - 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, - 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, - 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, - 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, - 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, - 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1, 0, - 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 89, 1, - 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 95, 1, 0, 0, 0, 0, 97, - 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, 0, 0, 0, 0, 103, 1, 0, 0, 0, - 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, 109, 1, 0, 0, 0, 0, 111, 1, - 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 115, 1, 0, 0, 0, 0, 121, 1, 0, 0, 0, 0, - 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 131, 1, 0, - 0, 0, 0, 133, 1, 0, 0, 0, 0, 135, 1, 0, 0, 0, 0, 137, 1, 0, 0, 0, 1, 139, - 1, 0, 0, 0, 3, 144, 1, 0, 0, 0, 5, 147, 1, 0, 0, 0, 7, 152, 1, 0, 0, 0, - 9, 159, 1, 0, 0, 0, 11, 163, 1, 0, 0, 0, 13, 169, 1, 0, 0, 0, 15, 178, - 1, 0, 0, 0, 17, 187, 1, 0, 0, 0, 19, 193, 1, 0, 0, 0, 21, 198, 1, 0, 0, - 0, 23, 203, 1, 0, 0, 0, 25, 213, 1, 0, 0, 0, 27, 216, 1, 0, 0, 0, 29, 220, - 1, 0, 0, 0, 31, 224, 1, 0, 0, 0, 33, 228, 1, 0, 0, 0, 35, 231, 1, 0, 0, - 0, 37, 235, 1, 0, 0, 0, 39, 239, 1, 0, 0, 0, 41, 243, 1, 0, 0, 0, 43, 247, - 1, 0, 0, 0, 45, 251, 1, 0, 0, 0, 47, 255, 1, 0, 0, 0, 49, 262, 1, 0, 0, - 0, 51, 265, 1, 0, 0, 0, 53, 268, 1, 0, 0, 0, 55, 271, 1, 0, 0, 0, 57, 274, - 1, 0, 0, 0, 59, 276, 1, 0, 0, 0, 61, 279, 1, 0, 0, 0, 63, 282, 1, 0, 0, - 0, 65, 285, 1, 0, 0, 0, 67, 288, 1, 0, 0, 0, 69, 291, 1, 0, 0, 0, 71, 293, - 1, 0, 0, 0, 73, 295, 1, 0, 0, 0, 75, 297, 1, 0, 0, 0, 77, 299, 1, 0, 0, - 0, 79, 301, 1, 0, 0, 0, 81, 303, 1, 0, 0, 0, 83, 306, 1, 0, 0, 0, 85, 309, - 1, 0, 0, 0, 87, 311, 1, 0, 0, 0, 89, 313, 1, 0, 0, 0, 91, 316, 1, 0, 0, - 0, 93, 319, 1, 0, 0, 0, 95, 323, 1, 0, 0, 0, 97, 326, 1, 0, 0, 0, 99, 330, - 1, 0, 0, 0, 101, 332, 1, 0, 0, 0, 103, 334, 1, 0, 0, 0, 105, 336, 1, 0, - 0, 0, 107, 338, 1, 0, 0, 0, 109, 340, 1, 0, 0, 0, 111, 342, 1, 0, 0, 0, - 113, 344, 1, 0, 0, 0, 115, 346, 1, 0, 0, 0, 117, 349, 1, 0, 0, 0, 119, - 353, 1, 0, 0, 0, 121, 355, 1, 0, 0, 0, 123, 364, 1, 0, 0, 0, 125, 366, - 1, 0, 0, 0, 127, 376, 1, 0, 0, 0, 129, 396, 1, 0, 0, 0, 131, 398, 1, 0, - 0, 0, 133, 405, 1, 0, 0, 0, 135, 416, 1, 0, 0, 0, 137, 431, 1, 0, 0, 0, - 139, 140, 5, 102, 0, 0, 140, 141, 5, 117, 0, 0, 141, 142, 5, 110, 0, 0, - 142, 143, 5, 99, 0, 0, 143, 2, 1, 0, 0, 0, 144, 145, 5, 105, 0, 0, 145, - 146, 5, 102, 0, 0, 146, 4, 1, 0, 0, 0, 147, 148, 5, 101, 0, 0, 148, 149, - 5, 108, 0, 0, 149, 150, 5, 115, 0, 0, 150, 151, 5, 101, 0, 0, 151, 6, 1, - 0, 0, 0, 152, 153, 5, 114, 0, 0, 153, 154, 5, 101, 0, 0, 154, 155, 5, 116, - 0, 0, 155, 156, 5, 117, 0, 0, 156, 157, 5, 114, 0, 0, 157, 158, 5, 110, - 0, 0, 158, 8, 1, 0, 0, 0, 159, 160, 5, 102, 0, 0, 160, 161, 5, 111, 0, - 0, 161, 162, 5, 114, 0, 0, 162, 10, 1, 0, 0, 0, 163, 164, 5, 98, 0, 0, - 164, 165, 5, 114, 0, 0, 165, 166, 5, 101, 0, 0, 166, 167, 5, 97, 0, 0, - 167, 168, 5, 107, 0, 0, 168, 12, 1, 0, 0, 0, 169, 170, 5, 99, 0, 0, 170, - 171, 5, 111, 0, 0, 171, 172, 5, 110, 0, 0, 172, 173, 5, 116, 0, 0, 173, - 174, 5, 105, 0, 0, 174, 175, 5, 110, 0, 0, 175, 176, 5, 117, 0, 0, 176, - 177, 5, 101, 0, 0, 177, 14, 1, 0, 0, 0, 178, 179, 5, 115, 0, 0, 179, 180, - 5, 101, 0, 0, 180, 181, 5, 113, 0, 0, 181, 182, 5, 117, 0, 0, 182, 183, - 5, 101, 0, 0, 183, 184, 5, 110, 0, 0, 184, 185, 5, 99, 0, 0, 185, 186, - 5, 101, 0, 0, 186, 16, 1, 0, 0, 0, 187, 188, 5, 115, 0, 0, 188, 189, 5, - 116, 0, 0, 189, 190, 5, 97, 0, 0, 190, 191, 5, 103, 0, 0, 191, 192, 5, - 101, 0, 0, 192, 18, 1, 0, 0, 0, 193, 194, 5, 110, 0, 0, 194, 195, 5, 101, - 0, 0, 195, 196, 5, 120, 0, 0, 196, 197, 5, 116, 0, 0, 197, 20, 1, 0, 0, - 0, 198, 199, 5, 99, 0, 0, 199, 200, 5, 104, 0, 0, 200, 201, 5, 97, 0, 0, - 201, 202, 5, 110, 0, 0, 202, 22, 1, 0, 0, 0, 203, 204, 5, 97, 0, 0, 204, - 205, 5, 117, 0, 0, 205, 206, 5, 116, 0, 0, 206, 207, 5, 104, 0, 0, 207, - 208, 5, 111, 0, 0, 208, 209, 5, 114, 0, 0, 209, 210, 5, 105, 0, 0, 210, - 211, 5, 116, 0, 0, 211, 212, 5, 121, 0, 0, 212, 24, 1, 0, 0, 0, 213, 214, - 5, 105, 0, 0, 214, 215, 5, 56, 0, 0, 215, 26, 1, 0, 0, 0, 216, 217, 5, - 105, 0, 0, 217, 218, 5, 49, 0, 0, 218, 219, 5, 54, 0, 0, 219, 28, 1, 0, - 0, 0, 220, 221, 5, 105, 0, 0, 221, 222, 5, 51, 0, 0, 222, 223, 5, 50, 0, - 0, 223, 30, 1, 0, 0, 0, 224, 225, 5, 105, 0, 0, 225, 226, 5, 54, 0, 0, - 226, 227, 5, 52, 0, 0, 227, 32, 1, 0, 0, 0, 228, 229, 5, 117, 0, 0, 229, - 230, 5, 56, 0, 0, 230, 34, 1, 0, 0, 0, 231, 232, 5, 117, 0, 0, 232, 233, - 5, 49, 0, 0, 233, 234, 5, 54, 0, 0, 234, 36, 1, 0, 0, 0, 235, 236, 5, 117, - 0, 0, 236, 237, 5, 51, 0, 0, 237, 238, 5, 50, 0, 0, 238, 38, 1, 0, 0, 0, - 239, 240, 5, 117, 0, 0, 240, 241, 5, 54, 0, 0, 241, 242, 5, 52, 0, 0, 242, - 40, 1, 0, 0, 0, 243, 244, 5, 102, 0, 0, 244, 245, 5, 51, 0, 0, 245, 246, - 5, 50, 0, 0, 246, 42, 1, 0, 0, 0, 247, 248, 5, 102, 0, 0, 248, 249, 5, - 54, 0, 0, 249, 250, 5, 52, 0, 0, 250, 44, 1, 0, 0, 0, 251, 252, 5, 115, - 0, 0, 252, 253, 5, 116, 0, 0, 253, 254, 5, 114, 0, 0, 254, 46, 1, 0, 0, - 0, 255, 256, 5, 115, 0, 0, 256, 257, 5, 101, 0, 0, 257, 258, 5, 114, 0, - 0, 258, 259, 5, 105, 0, 0, 259, 260, 5, 101, 0, 0, 260, 261, 5, 115, 0, - 0, 261, 48, 1, 0, 0, 0, 262, 263, 5, 45, 0, 0, 263, 264, 5, 62, 0, 0, 264, - 50, 1, 0, 0, 0, 265, 266, 5, 58, 0, 0, 266, 267, 5, 61, 0, 0, 267, 52, - 1, 0, 0, 0, 268, 269, 5, 36, 0, 0, 269, 270, 5, 61, 0, 0, 270, 54, 1, 0, - 0, 0, 271, 272, 5, 61, 0, 0, 272, 273, 5, 62, 0, 0, 273, 56, 1, 0, 0, 0, - 274, 275, 5, 61, 0, 0, 275, 58, 1, 0, 0, 0, 276, 277, 5, 43, 0, 0, 277, - 278, 5, 61, 0, 0, 278, 60, 1, 0, 0, 0, 279, 280, 5, 45, 0, 0, 280, 281, - 5, 61, 0, 0, 281, 62, 1, 0, 0, 0, 282, 283, 5, 42, 0, 0, 283, 284, 5, 61, - 0, 0, 284, 64, 1, 0, 0, 0, 285, 286, 5, 47, 0, 0, 286, 287, 5, 61, 0, 0, - 287, 66, 1, 0, 0, 0, 288, 289, 5, 37, 0, 0, 289, 290, 5, 61, 0, 0, 290, - 68, 1, 0, 0, 0, 291, 292, 5, 43, 0, 0, 292, 70, 1, 0, 0, 0, 293, 294, 5, - 45, 0, 0, 294, 72, 1, 0, 0, 0, 295, 296, 5, 42, 0, 0, 296, 74, 1, 0, 0, - 0, 297, 298, 5, 47, 0, 0, 298, 76, 1, 0, 0, 0, 299, 300, 5, 37, 0, 0, 300, - 78, 1, 0, 0, 0, 301, 302, 5, 94, 0, 0, 302, 80, 1, 0, 0, 0, 303, 304, 5, - 61, 0, 0, 304, 305, 5, 61, 0, 0, 305, 82, 1, 0, 0, 0, 306, 307, 5, 33, - 0, 0, 307, 308, 5, 61, 0, 0, 308, 84, 1, 0, 0, 0, 309, 310, 5, 60, 0, 0, - 310, 86, 1, 0, 0, 0, 311, 312, 5, 62, 0, 0, 312, 88, 1, 0, 0, 0, 313, 314, - 5, 60, 0, 0, 314, 315, 5, 61, 0, 0, 315, 90, 1, 0, 0, 0, 316, 317, 5, 62, - 0, 0, 317, 318, 5, 61, 0, 0, 318, 92, 1, 0, 0, 0, 319, 320, 5, 97, 0, 0, - 320, 321, 5, 110, 0, 0, 321, 322, 5, 100, 0, 0, 322, 94, 1, 0, 0, 0, 323, - 324, 5, 111, 0, 0, 324, 325, 5, 114, 0, 0, 325, 96, 1, 0, 0, 0, 326, 327, - 5, 110, 0, 0, 327, 328, 5, 111, 0, 0, 328, 329, 5, 116, 0, 0, 329, 98, - 1, 0, 0, 0, 330, 331, 5, 40, 0, 0, 331, 100, 1, 0, 0, 0, 332, 333, 5, 41, - 0, 0, 333, 102, 1, 0, 0, 0, 334, 335, 5, 123, 0, 0, 335, 104, 1, 0, 0, - 0, 336, 337, 5, 125, 0, 0, 337, 106, 1, 0, 0, 0, 338, 339, 5, 91, 0, 0, - 339, 108, 1, 0, 0, 0, 340, 341, 5, 93, 0, 0, 341, 110, 1, 0, 0, 0, 342, - 343, 5, 44, 0, 0, 343, 112, 1, 0, 0, 0, 344, 345, 5, 58, 0, 0, 345, 114, - 1, 0, 0, 0, 346, 347, 5, 46, 0, 0, 347, 116, 1, 0, 0, 0, 348, 350, 3, 119, - 59, 0, 349, 348, 1, 0, 0, 0, 350, 351, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, - 351, 352, 1, 0, 0, 0, 352, 118, 1, 0, 0, 0, 353, 354, 7, 0, 0, 0, 354, - 120, 1, 0, 0, 0, 355, 356, 3, 117, 58, 0, 356, 122, 1, 0, 0, 0, 357, 358, - 3, 117, 58, 0, 358, 360, 5, 46, 0, 0, 359, 361, 3, 117, 58, 0, 360, 359, - 1, 0, 0, 0, 360, 361, 1, 0, 0, 0, 361, 365, 1, 0, 0, 0, 362, 363, 5, 46, - 0, 0, 363, 365, 3, 117, 58, 0, 364, 357, 1, 0, 0, 0, 364, 362, 1, 0, 0, - 0, 365, 124, 1, 0, 0, 0, 366, 371, 5, 34, 0, 0, 367, 370, 8, 1, 0, 0, 368, - 370, 3, 129, 64, 0, 369, 367, 1, 0, 0, 0, 369, 368, 1, 0, 0, 0, 370, 373, - 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 371, 372, 1, 0, 0, 0, 372, 374, 1, 0, - 0, 0, 373, 371, 1, 0, 0, 0, 374, 375, 5, 34, 0, 0, 375, 126, 1, 0, 0, 0, - 376, 382, 5, 96, 0, 0, 377, 378, 5, 92, 0, 0, 378, 381, 5, 96, 0, 0, 379, - 381, 8, 2, 0, 0, 380, 377, 1, 0, 0, 0, 380, 379, 1, 0, 0, 0, 381, 384, - 1, 0, 0, 0, 382, 380, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 385, 1, 0, - 0, 0, 384, 382, 1, 0, 0, 0, 385, 386, 5, 96, 0, 0, 386, 128, 1, 0, 0, 0, - 387, 388, 5, 92, 0, 0, 388, 397, 7, 3, 0, 0, 389, 390, 5, 92, 0, 0, 390, - 391, 5, 117, 0, 0, 391, 392, 1, 0, 0, 0, 392, 393, 7, 4, 0, 0, 393, 394, - 7, 4, 0, 0, 394, 395, 7, 4, 0, 0, 395, 397, 7, 4, 0, 0, 396, 387, 1, 0, - 0, 0, 396, 389, 1, 0, 0, 0, 397, 130, 1, 0, 0, 0, 398, 402, 7, 5, 0, 0, - 399, 401, 7, 6, 0, 0, 400, 399, 1, 0, 0, 0, 401, 404, 1, 0, 0, 0, 402, - 400, 1, 0, 0, 0, 402, 403, 1, 0, 0, 0, 403, 132, 1, 0, 0, 0, 404, 402, - 1, 0, 0, 0, 405, 406, 5, 47, 0, 0, 406, 407, 5, 47, 0, 0, 407, 411, 1, - 0, 0, 0, 408, 410, 8, 7, 0, 0, 409, 408, 1, 0, 0, 0, 410, 413, 1, 0, 0, - 0, 411, 409, 1, 0, 0, 0, 411, 412, 1, 0, 0, 0, 412, 414, 1, 0, 0, 0, 413, - 411, 1, 0, 0, 0, 414, 415, 6, 66, 0, 0, 415, 134, 1, 0, 0, 0, 416, 417, - 5, 47, 0, 0, 417, 418, 5, 42, 0, 0, 418, 422, 1, 0, 0, 0, 419, 421, 9, - 0, 0, 0, 420, 419, 1, 0, 0, 0, 421, 424, 1, 0, 0, 0, 422, 423, 1, 0, 0, - 0, 422, 420, 1, 0, 0, 0, 423, 425, 1, 0, 0, 0, 424, 422, 1, 0, 0, 0, 425, - 426, 5, 42, 0, 0, 426, 427, 5, 47, 0, 0, 427, 428, 1, 0, 0, 0, 428, 429, - 6, 67, 0, 0, 429, 136, 1, 0, 0, 0, 430, 432, 7, 8, 0, 0, 431, 430, 1, 0, - 0, 0, 432, 433, 1, 0, 0, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, - 434, 435, 1, 0, 0, 0, 435, 436, 6, 68, 0, 0, 436, 138, 1, 0, 0, 0, 13, - 0, 351, 360, 364, 369, 371, 380, 382, 396, 402, 411, 422, 433, 1, 0, 1, - 0, + 1, 61, 1, 61, 3, 61, 365, 8, 61, 1, 62, 3, 62, 368, 8, 62, 1, 62, 1, 62, + 1, 62, 1, 62, 5, 62, 374, 8, 62, 10, 62, 12, 62, 377, 9, 62, 1, 62, 1, + 62, 1, 63, 3, 63, 382, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 5, 63, 388, 8, + 63, 10, 63, 12, 63, 391, 9, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, + 1, 64, 3, 64, 400, 8, 64, 1, 65, 1, 65, 5, 65, 404, 8, 65, 10, 65, 12, + 65, 407, 9, 65, 1, 66, 1, 66, 1, 66, 1, 66, 5, 66, 413, 8, 66, 10, 66, + 12, 66, 416, 9, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 5, 67, 424, + 8, 67, 10, 67, 12, 67, 427, 9, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, + 68, 4, 68, 435, 8, 68, 11, 68, 12, 68, 436, 1, 68, 1, 68, 1, 425, 0, 69, + 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, + 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, + 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29, + 59, 30, 61, 31, 63, 32, 65, 33, 67, 34, 69, 35, 71, 36, 73, 37, 75, 38, + 77, 39, 79, 40, 81, 41, 83, 42, 85, 43, 87, 44, 89, 45, 91, 46, 93, 47, + 95, 48, 97, 49, 99, 50, 101, 51, 103, 52, 105, 53, 107, 54, 109, 55, 111, + 56, 113, 57, 115, 58, 117, 0, 119, 0, 121, 59, 123, 60, 125, 61, 127, 62, + 129, 0, 131, 63, 133, 64, 135, 65, 137, 66, 1, 0, 8, 1, 0, 48, 57, 2, 0, + 92, 92, 96, 96, 4, 0, 10, 10, 13, 13, 34, 34, 92, 92, 2, 0, 102, 102, 114, + 114, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, + 2, 0, 10, 10, 13, 13, 3, 0, 9, 10, 13, 13, 32, 32, 451, 0, 1, 1, 0, 0, + 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, + 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, + 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, + 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, + 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, + 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, + 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, + 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, + 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, + 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, + 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, + 87, 1, 0, 0, 0, 0, 89, 1, 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, 0, + 0, 95, 1, 0, 0, 0, 0, 97, 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, 0, + 0, 0, 0, 103, 1, 0, 0, 0, 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, 109, + 1, 0, 0, 0, 0, 111, 1, 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 115, 1, 0, 0, 0, + 0, 121, 1, 0, 0, 0, 0, 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, + 0, 0, 0, 0, 131, 1, 0, 0, 0, 0, 133, 1, 0, 0, 0, 0, 135, 1, 0, 0, 0, 0, + 137, 1, 0, 0, 0, 1, 139, 1, 0, 0, 0, 3, 144, 1, 0, 0, 0, 5, 147, 1, 0, + 0, 0, 7, 152, 1, 0, 0, 0, 9, 159, 1, 0, 0, 0, 11, 163, 1, 0, 0, 0, 13, + 169, 1, 0, 0, 0, 15, 178, 1, 0, 0, 0, 17, 187, 1, 0, 0, 0, 19, 193, 1, + 0, 0, 0, 21, 198, 1, 0, 0, 0, 23, 203, 1, 0, 0, 0, 25, 213, 1, 0, 0, 0, + 27, 216, 1, 0, 0, 0, 29, 220, 1, 0, 0, 0, 31, 224, 1, 0, 0, 0, 33, 228, + 1, 0, 0, 0, 35, 231, 1, 0, 0, 0, 37, 235, 1, 0, 0, 0, 39, 239, 1, 0, 0, + 0, 41, 243, 1, 0, 0, 0, 43, 247, 1, 0, 0, 0, 45, 251, 1, 0, 0, 0, 47, 255, + 1, 0, 0, 0, 49, 262, 1, 0, 0, 0, 51, 265, 1, 0, 0, 0, 53, 268, 1, 0, 0, + 0, 55, 271, 1, 0, 0, 0, 57, 274, 1, 0, 0, 0, 59, 276, 1, 0, 0, 0, 61, 279, + 1, 0, 0, 0, 63, 282, 1, 0, 0, 0, 65, 285, 1, 0, 0, 0, 67, 288, 1, 0, 0, + 0, 69, 291, 1, 0, 0, 0, 71, 293, 1, 0, 0, 0, 73, 295, 1, 0, 0, 0, 75, 297, + 1, 0, 0, 0, 77, 299, 1, 0, 0, 0, 79, 301, 1, 0, 0, 0, 81, 303, 1, 0, 0, + 0, 83, 306, 1, 0, 0, 0, 85, 309, 1, 0, 0, 0, 87, 311, 1, 0, 0, 0, 89, 313, + 1, 0, 0, 0, 91, 316, 1, 0, 0, 0, 93, 319, 1, 0, 0, 0, 95, 323, 1, 0, 0, + 0, 97, 326, 1, 0, 0, 0, 99, 330, 1, 0, 0, 0, 101, 332, 1, 0, 0, 0, 103, + 334, 1, 0, 0, 0, 105, 336, 1, 0, 0, 0, 107, 338, 1, 0, 0, 0, 109, 340, + 1, 0, 0, 0, 111, 342, 1, 0, 0, 0, 113, 344, 1, 0, 0, 0, 115, 346, 1, 0, + 0, 0, 117, 349, 1, 0, 0, 0, 119, 353, 1, 0, 0, 0, 121, 355, 1, 0, 0, 0, + 123, 364, 1, 0, 0, 0, 125, 367, 1, 0, 0, 0, 127, 381, 1, 0, 0, 0, 129, + 399, 1, 0, 0, 0, 131, 401, 1, 0, 0, 0, 133, 408, 1, 0, 0, 0, 135, 419, + 1, 0, 0, 0, 137, 434, 1, 0, 0, 0, 139, 140, 5, 102, 0, 0, 140, 141, 5, + 117, 0, 0, 141, 142, 5, 110, 0, 0, 142, 143, 5, 99, 0, 0, 143, 2, 1, 0, + 0, 0, 144, 145, 5, 105, 0, 0, 145, 146, 5, 102, 0, 0, 146, 4, 1, 0, 0, + 0, 147, 148, 5, 101, 0, 0, 148, 149, 5, 108, 0, 0, 149, 150, 5, 115, 0, + 0, 150, 151, 5, 101, 0, 0, 151, 6, 1, 0, 0, 0, 152, 153, 5, 114, 0, 0, + 153, 154, 5, 101, 0, 0, 154, 155, 5, 116, 0, 0, 155, 156, 5, 117, 0, 0, + 156, 157, 5, 114, 0, 0, 157, 158, 5, 110, 0, 0, 158, 8, 1, 0, 0, 0, 159, + 160, 5, 102, 0, 0, 160, 161, 5, 111, 0, 0, 161, 162, 5, 114, 0, 0, 162, + 10, 1, 0, 0, 0, 163, 164, 5, 98, 0, 0, 164, 165, 5, 114, 0, 0, 165, 166, + 5, 101, 0, 0, 166, 167, 5, 97, 0, 0, 167, 168, 5, 107, 0, 0, 168, 12, 1, + 0, 0, 0, 169, 170, 5, 99, 0, 0, 170, 171, 5, 111, 0, 0, 171, 172, 5, 110, + 0, 0, 172, 173, 5, 116, 0, 0, 173, 174, 5, 105, 0, 0, 174, 175, 5, 110, + 0, 0, 175, 176, 5, 117, 0, 0, 176, 177, 5, 101, 0, 0, 177, 14, 1, 0, 0, + 0, 178, 179, 5, 115, 0, 0, 179, 180, 5, 101, 0, 0, 180, 181, 5, 113, 0, + 0, 181, 182, 5, 117, 0, 0, 182, 183, 5, 101, 0, 0, 183, 184, 5, 110, 0, + 0, 184, 185, 5, 99, 0, 0, 185, 186, 5, 101, 0, 0, 186, 16, 1, 0, 0, 0, + 187, 188, 5, 115, 0, 0, 188, 189, 5, 116, 0, 0, 189, 190, 5, 97, 0, 0, + 190, 191, 5, 103, 0, 0, 191, 192, 5, 101, 0, 0, 192, 18, 1, 0, 0, 0, 193, + 194, 5, 110, 0, 0, 194, 195, 5, 101, 0, 0, 195, 196, 5, 120, 0, 0, 196, + 197, 5, 116, 0, 0, 197, 20, 1, 0, 0, 0, 198, 199, 5, 99, 0, 0, 199, 200, + 5, 104, 0, 0, 200, 201, 5, 97, 0, 0, 201, 202, 5, 110, 0, 0, 202, 22, 1, + 0, 0, 0, 203, 204, 5, 97, 0, 0, 204, 205, 5, 117, 0, 0, 205, 206, 5, 116, + 0, 0, 206, 207, 5, 104, 0, 0, 207, 208, 5, 111, 0, 0, 208, 209, 5, 114, + 0, 0, 209, 210, 5, 105, 0, 0, 210, 211, 5, 116, 0, 0, 211, 212, 5, 121, + 0, 0, 212, 24, 1, 0, 0, 0, 213, 214, 5, 105, 0, 0, 214, 215, 5, 56, 0, + 0, 215, 26, 1, 0, 0, 0, 216, 217, 5, 105, 0, 0, 217, 218, 5, 49, 0, 0, + 218, 219, 5, 54, 0, 0, 219, 28, 1, 0, 0, 0, 220, 221, 5, 105, 0, 0, 221, + 222, 5, 51, 0, 0, 222, 223, 5, 50, 0, 0, 223, 30, 1, 0, 0, 0, 224, 225, + 5, 105, 0, 0, 225, 226, 5, 54, 0, 0, 226, 227, 5, 52, 0, 0, 227, 32, 1, + 0, 0, 0, 228, 229, 5, 117, 0, 0, 229, 230, 5, 56, 0, 0, 230, 34, 1, 0, + 0, 0, 231, 232, 5, 117, 0, 0, 232, 233, 5, 49, 0, 0, 233, 234, 5, 54, 0, + 0, 234, 36, 1, 0, 0, 0, 235, 236, 5, 117, 0, 0, 236, 237, 5, 51, 0, 0, + 237, 238, 5, 50, 0, 0, 238, 38, 1, 0, 0, 0, 239, 240, 5, 117, 0, 0, 240, + 241, 5, 54, 0, 0, 241, 242, 5, 52, 0, 0, 242, 40, 1, 0, 0, 0, 243, 244, + 5, 102, 0, 0, 244, 245, 5, 51, 0, 0, 245, 246, 5, 50, 0, 0, 246, 42, 1, + 0, 0, 0, 247, 248, 5, 102, 0, 0, 248, 249, 5, 54, 0, 0, 249, 250, 5, 52, + 0, 0, 250, 44, 1, 0, 0, 0, 251, 252, 5, 115, 0, 0, 252, 253, 5, 116, 0, + 0, 253, 254, 5, 114, 0, 0, 254, 46, 1, 0, 0, 0, 255, 256, 5, 115, 0, 0, + 256, 257, 5, 101, 0, 0, 257, 258, 5, 114, 0, 0, 258, 259, 5, 105, 0, 0, + 259, 260, 5, 101, 0, 0, 260, 261, 5, 115, 0, 0, 261, 48, 1, 0, 0, 0, 262, + 263, 5, 45, 0, 0, 263, 264, 5, 62, 0, 0, 264, 50, 1, 0, 0, 0, 265, 266, + 5, 58, 0, 0, 266, 267, 5, 61, 0, 0, 267, 52, 1, 0, 0, 0, 268, 269, 5, 36, + 0, 0, 269, 270, 5, 61, 0, 0, 270, 54, 1, 0, 0, 0, 271, 272, 5, 61, 0, 0, + 272, 273, 5, 62, 0, 0, 273, 56, 1, 0, 0, 0, 274, 275, 5, 61, 0, 0, 275, + 58, 1, 0, 0, 0, 276, 277, 5, 43, 0, 0, 277, 278, 5, 61, 0, 0, 278, 60, + 1, 0, 0, 0, 279, 280, 5, 45, 0, 0, 280, 281, 5, 61, 0, 0, 281, 62, 1, 0, + 0, 0, 282, 283, 5, 42, 0, 0, 283, 284, 5, 61, 0, 0, 284, 64, 1, 0, 0, 0, + 285, 286, 5, 47, 0, 0, 286, 287, 5, 61, 0, 0, 287, 66, 1, 0, 0, 0, 288, + 289, 5, 37, 0, 0, 289, 290, 5, 61, 0, 0, 290, 68, 1, 0, 0, 0, 291, 292, + 5, 43, 0, 0, 292, 70, 1, 0, 0, 0, 293, 294, 5, 45, 0, 0, 294, 72, 1, 0, + 0, 0, 295, 296, 5, 42, 0, 0, 296, 74, 1, 0, 0, 0, 297, 298, 5, 47, 0, 0, + 298, 76, 1, 0, 0, 0, 299, 300, 5, 37, 0, 0, 300, 78, 1, 0, 0, 0, 301, 302, + 5, 94, 0, 0, 302, 80, 1, 0, 0, 0, 303, 304, 5, 61, 0, 0, 304, 305, 5, 61, + 0, 0, 305, 82, 1, 0, 0, 0, 306, 307, 5, 33, 0, 0, 307, 308, 5, 61, 0, 0, + 308, 84, 1, 0, 0, 0, 309, 310, 5, 60, 0, 0, 310, 86, 1, 0, 0, 0, 311, 312, + 5, 62, 0, 0, 312, 88, 1, 0, 0, 0, 313, 314, 5, 60, 0, 0, 314, 315, 5, 61, + 0, 0, 315, 90, 1, 0, 0, 0, 316, 317, 5, 62, 0, 0, 317, 318, 5, 61, 0, 0, + 318, 92, 1, 0, 0, 0, 319, 320, 5, 97, 0, 0, 320, 321, 5, 110, 0, 0, 321, + 322, 5, 100, 0, 0, 322, 94, 1, 0, 0, 0, 323, 324, 5, 111, 0, 0, 324, 325, + 5, 114, 0, 0, 325, 96, 1, 0, 0, 0, 326, 327, 5, 110, 0, 0, 327, 328, 5, + 111, 0, 0, 328, 329, 5, 116, 0, 0, 329, 98, 1, 0, 0, 0, 330, 331, 5, 40, + 0, 0, 331, 100, 1, 0, 0, 0, 332, 333, 5, 41, 0, 0, 333, 102, 1, 0, 0, 0, + 334, 335, 5, 123, 0, 0, 335, 104, 1, 0, 0, 0, 336, 337, 5, 125, 0, 0, 337, + 106, 1, 0, 0, 0, 338, 339, 5, 91, 0, 0, 339, 108, 1, 0, 0, 0, 340, 341, + 5, 93, 0, 0, 341, 110, 1, 0, 0, 0, 342, 343, 5, 44, 0, 0, 343, 112, 1, + 0, 0, 0, 344, 345, 5, 58, 0, 0, 345, 114, 1, 0, 0, 0, 346, 347, 5, 46, + 0, 0, 347, 116, 1, 0, 0, 0, 348, 350, 3, 119, 59, 0, 349, 348, 1, 0, 0, + 0, 350, 351, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, + 118, 1, 0, 0, 0, 353, 354, 7, 0, 0, 0, 354, 120, 1, 0, 0, 0, 355, 356, + 3, 117, 58, 0, 356, 122, 1, 0, 0, 0, 357, 358, 3, 117, 58, 0, 358, 360, + 5, 46, 0, 0, 359, 361, 3, 117, 58, 0, 360, 359, 1, 0, 0, 0, 360, 361, 1, + 0, 0, 0, 361, 365, 1, 0, 0, 0, 362, 363, 5, 46, 0, 0, 363, 365, 3, 117, + 58, 0, 364, 357, 1, 0, 0, 0, 364, 362, 1, 0, 0, 0, 365, 124, 1, 0, 0, 0, + 366, 368, 3, 129, 64, 0, 367, 366, 1, 0, 0, 0, 367, 368, 1, 0, 0, 0, 368, + 369, 1, 0, 0, 0, 369, 375, 5, 96, 0, 0, 370, 374, 8, 1, 0, 0, 371, 372, + 5, 92, 0, 0, 372, 374, 9, 0, 0, 0, 373, 370, 1, 0, 0, 0, 373, 371, 1, 0, + 0, 0, 374, 377, 1, 0, 0, 0, 375, 373, 1, 0, 0, 0, 375, 376, 1, 0, 0, 0, + 376, 378, 1, 0, 0, 0, 377, 375, 1, 0, 0, 0, 378, 379, 5, 96, 0, 0, 379, + 126, 1, 0, 0, 0, 380, 382, 3, 129, 64, 0, 381, 380, 1, 0, 0, 0, 381, 382, + 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 389, 5, 34, 0, 0, 384, 388, 8, 2, + 0, 0, 385, 386, 5, 92, 0, 0, 386, 388, 9, 0, 0, 0, 387, 384, 1, 0, 0, 0, + 387, 385, 1, 0, 0, 0, 388, 391, 1, 0, 0, 0, 389, 387, 1, 0, 0, 0, 389, + 390, 1, 0, 0, 0, 390, 392, 1, 0, 0, 0, 391, 389, 1, 0, 0, 0, 392, 393, + 5, 34, 0, 0, 393, 128, 1, 0, 0, 0, 394, 400, 7, 3, 0, 0, 395, 396, 5, 114, + 0, 0, 396, 400, 5, 102, 0, 0, 397, 398, 5, 102, 0, 0, 398, 400, 5, 114, + 0, 0, 399, 394, 1, 0, 0, 0, 399, 395, 1, 0, 0, 0, 399, 397, 1, 0, 0, 0, + 400, 130, 1, 0, 0, 0, 401, 405, 7, 4, 0, 0, 402, 404, 7, 5, 0, 0, 403, + 402, 1, 0, 0, 0, 404, 407, 1, 0, 0, 0, 405, 403, 1, 0, 0, 0, 405, 406, + 1, 0, 0, 0, 406, 132, 1, 0, 0, 0, 407, 405, 1, 0, 0, 0, 408, 409, 5, 47, + 0, 0, 409, 410, 5, 47, 0, 0, 410, 414, 1, 0, 0, 0, 411, 413, 8, 6, 0, 0, + 412, 411, 1, 0, 0, 0, 413, 416, 1, 0, 0, 0, 414, 412, 1, 0, 0, 0, 414, + 415, 1, 0, 0, 0, 415, 417, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 417, 418, + 6, 66, 0, 0, 418, 134, 1, 0, 0, 0, 419, 420, 5, 47, 0, 0, 420, 421, 5, + 42, 0, 0, 421, 425, 1, 0, 0, 0, 422, 424, 9, 0, 0, 0, 423, 422, 1, 0, 0, + 0, 424, 427, 1, 0, 0, 0, 425, 426, 1, 0, 0, 0, 425, 423, 1, 0, 0, 0, 426, + 428, 1, 0, 0, 0, 427, 425, 1, 0, 0, 0, 428, 429, 5, 42, 0, 0, 429, 430, + 5, 47, 0, 0, 430, 431, 1, 0, 0, 0, 431, 432, 6, 67, 0, 0, 432, 136, 1, + 0, 0, 0, 433, 435, 7, 7, 0, 0, 434, 433, 1, 0, 0, 0, 435, 436, 1, 0, 0, + 0, 436, 434, 1, 0, 0, 0, 436, 437, 1, 0, 0, 0, 437, 438, 1, 0, 0, 0, 438, + 439, 6, 68, 0, 0, 439, 138, 1, 0, 0, 0, 15, 0, 351, 360, 364, 367, 373, + 375, 381, 387, 389, 399, 405, 414, 425, 436, 1, 0, 1, 0, } deserializer := antlr.NewATNDeserializer(nil) staticData.atn = deserializer.Deserialize(staticData.serializedATN) @@ -379,8 +380,8 @@ const ( ArcLexerDOT = 58 ArcLexerINTEGER_LITERAL = 59 ArcLexerFLOAT_LITERAL = 60 - ArcLexerSTR_LITERAL = 61 - ArcLexerSTR_LITERAL_RAW = 62 + ArcLexerSTR_LITERAL_MULTI = 61 + ArcLexerSTR_LITERAL = 62 ArcLexerIDENTIFIER = 63 ArcLexerSINGLE_LINE_COMMENT = 64 ArcLexerMULTI_LINE_COMMENT = 65 diff --git a/arc/go/parser/arc_parser.go b/arc/go/parser/arc_parser.go index f41a67bde9..16c366faa6 100644 --- a/arc/go/parser/arc_parser.go +++ b/arc/go/parser/arc_parser.go @@ -59,7 +59,7 @@ func arcparserParserInit() { "SLASH", "PERCENT", "CARET", "EQ", "NEQ", "LT", "GT", "LEQ", "GEQ", "AND", "OR", "NOT", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET", "COMMA", "COLON", "DOT", "INTEGER_LITERAL", "FLOAT_LITERAL", - "STR_LITERAL", "STR_LITERAL_RAW", "IDENTIFIER", "SINGLE_LINE_COMMENT", + "STR_LITERAL_MULTI", "STR_LITERAL", "IDENTIFIER", "SINGLE_LINE_COMMENT", "MULTI_LINE_COMMENT", "WS", } staticData.RuleNames = []string{ @@ -398,7 +398,7 @@ func arcparserParserInit() { 700, 694, 1, 0, 0, 0, 700, 695, 1, 0, 0, 0, 700, 699, 1, 0, 0, 0, 701, 139, 1, 0, 0, 0, 702, 703, 3, 98, 49, 0, 703, 704, 5, 50, 0, 0, 704, 705, 3, 114, 57, 0, 705, 706, 5, 51, 0, 0, 706, 141, 1, 0, 0, 0, 707, 712, 3, - 144, 72, 0, 708, 712, 5, 61, 0, 0, 709, 712, 5, 62, 0, 0, 710, 712, 3, + 144, 72, 0, 708, 712, 5, 62, 0, 0, 709, 712, 5, 61, 0, 0, 710, 712, 3, 146, 73, 0, 711, 707, 1, 0, 0, 0, 711, 708, 1, 0, 0, 0, 711, 709, 1, 0, 0, 0, 711, 710, 1, 0, 0, 0, 712, 143, 1, 0, 0, 0, 713, 715, 5, 36, 0, 0, 714, 713, 1, 0, 0, 0, 714, 715, 1, 0, 0, 0, 715, 716, 1, 0, 0, 0, 716, @@ -514,8 +514,8 @@ const ( ArcParserDOT = 58 ArcParserINTEGER_LITERAL = 59 ArcParserFLOAT_LITERAL = 60 - ArcParserSTR_LITERAL = 61 - ArcParserSTR_LITERAL_RAW = 62 + ArcParserSTR_LITERAL_MULTI = 61 + ArcParserSTR_LITERAL = 62 ArcParserIDENTIFIER = 63 ArcParserSINGLE_LINE_COMMENT = 64 ArcParserMULTI_LINE_COMMENT = 65 @@ -4733,7 +4733,7 @@ func (p *ArcParser) FlowStatement() (localctx IFlowStatementContext) { p.RoutingTable() } - case ArcParserNEXT, ArcParserCHAN, ArcParserAUTHORITY, ArcParserI8, ArcParserI16, ArcParserI32, ArcParserI64, ArcParserU8, ArcParserU16, ArcParserU32, ArcParserU64, ArcParserF32, ArcParserF64, ArcParserSTR, ArcParserSERIES, ArcParserMINUS, ArcParserNOT, ArcParserLPAREN, ArcParserLBRACKET, ArcParserINTEGER_LITERAL, ArcParserFLOAT_LITERAL, ArcParserSTR_LITERAL, ArcParserSTR_LITERAL_RAW, ArcParserIDENTIFIER: + case ArcParserNEXT, ArcParserCHAN, ArcParserAUTHORITY, ArcParserI8, ArcParserI16, ArcParserI32, ArcParserI64, ArcParserU8, ArcParserU16, ArcParserU32, ArcParserU64, ArcParserF32, ArcParserF64, ArcParserSTR, ArcParserSERIES, ArcParserMINUS, ArcParserNOT, ArcParserLPAREN, ArcParserLBRACKET, ArcParserINTEGER_LITERAL, ArcParserFLOAT_LITERAL, ArcParserSTR_LITERAL_MULTI, ArcParserSTR_LITERAL, ArcParserIDENTIFIER: { p.SetState(334) p.FlowNode() @@ -4768,7 +4768,7 @@ func (p *ArcParser) FlowStatement() (localctx IFlowStatementContext) { p.RoutingTable() } - case ArcParserNEXT, ArcParserCHAN, ArcParserAUTHORITY, ArcParserI8, ArcParserI16, ArcParserI32, ArcParserI64, ArcParserU8, ArcParserU16, ArcParserU32, ArcParserU64, ArcParserF32, ArcParserF64, ArcParserSTR, ArcParserSERIES, ArcParserMINUS, ArcParserNOT, ArcParserLPAREN, ArcParserLBRACKET, ArcParserINTEGER_LITERAL, ArcParserFLOAT_LITERAL, ArcParserSTR_LITERAL, ArcParserSTR_LITERAL_RAW, ArcParserIDENTIFIER: + case ArcParserNEXT, ArcParserCHAN, ArcParserAUTHORITY, ArcParserI8, ArcParserI16, ArcParserI32, ArcParserI64, ArcParserU8, ArcParserU16, ArcParserU32, ArcParserU64, ArcParserF32, ArcParserF64, ArcParserSTR, ArcParserSERIES, ArcParserMINUS, ArcParserNOT, ArcParserLPAREN, ArcParserLBRACKET, ArcParserINTEGER_LITERAL, ArcParserFLOAT_LITERAL, ArcParserSTR_LITERAL_MULTI, ArcParserSTR_LITERAL, ArcParserIDENTIFIER: { p.SetState(339) p.FlowNode() @@ -13752,7 +13752,7 @@ type ILiteralContext interface { // Getter signatures NumericLiteral() INumericLiteralContext STR_LITERAL() antlr.TerminalNode - STR_LITERAL_RAW() antlr.TerminalNode + STR_LITERAL_MULTI() antlr.TerminalNode SeriesLiteral() ISeriesLiteralContext // IsLiteralContext differentiates from other interfaces. @@ -13811,8 +13811,8 @@ func (s *LiteralContext) STR_LITERAL() antlr.TerminalNode { return s.GetToken(ArcParserSTR_LITERAL, 0) } -func (s *LiteralContext) STR_LITERAL_RAW() antlr.TerminalNode { - return s.GetToken(ArcParserSTR_LITERAL_RAW, 0) +func (s *LiteralContext) STR_LITERAL_MULTI() antlr.TerminalNode { + return s.GetToken(ArcParserSTR_LITERAL_MULTI, 0) } func (s *LiteralContext) SeriesLiteral() ISeriesLiteralContext { @@ -13889,11 +13889,11 @@ func (p *ArcParser) Literal() (localctx ILiteralContext) { } } - case ArcParserSTR_LITERAL_RAW: + case ArcParserSTR_LITERAL_MULTI: p.EnterOuterAlt(localctx, 3) { p.SetState(709) - p.Match(ArcParserSTR_LITERAL_RAW) + p.Match(ArcParserSTR_LITERAL_MULTI) if p.HasError() { // Recognition error - abort rule goto errorExit diff --git a/arc/go/parser/ast.go b/arc/go/parser/ast.go index 5a538d680f..e4e9484953 100644 --- a/arc/go/parser/ast.go +++ b/arc/go/parser/ast.go @@ -156,6 +156,19 @@ func GetLiteralNode(node antlr.ParserRuleContext) ILiteralContext { return nil } +// StringTerminal returns the string literal terminal node for either +// STR_LITERAL or STR_LITERAL_MULTI form, or +// nil if the literal is not a string. +func StringTerminal(lit ILiteralContext) antlr.TerminalNode { + if t := lit.STR_LITERAL(); t != nil { + return t + } + if t := lit.STR_LITERAL_MULTI(); t != nil { + return t + } + return nil +} + // IsNumericLiteral checks if an expression is a numeric literal (int or float), // possibly with a unary minus. This is more permissive than IsLiteral for cases // like [-1, -2.0] where we want to treat negated numbers as literals. diff --git a/arc/go/parser/ast_test.go b/arc/go/parser/ast_test.go index a87b825250..91e1b7920c 100644 --- a/arc/go/parser/ast_test.go +++ b/arc/go/parser/ast_test.go @@ -27,10 +27,11 @@ var _ = Describe("AST Utilities", func() { Entry("integer", "42"), Entry("float", "3.14"), Entry("string", `"hello"`), - Entry("raw string", "`hello`"), - Entry("empty raw string", "``"), - Entry("multi-line raw string", "`a\nb`"), - Entry("raw string with escaped backtick", "`say \\`hi\\``"), + Entry("raw string", `r"hello"`), + Entry("format string", `f"hi {x}"`), + Entry("multi-line string", "`a\nb`"), + Entry("multi-line format string", "f`hi {x}\nbye`"), + Entry("raw format string", `rf"path: {p}"`), Entry("unit literal", "5ms"), Entry("negated integer", "-1"), Entry("negated float", "-3.14"), @@ -80,9 +81,9 @@ var _ = Describe("AST Utilities", func() { Entry("integer", "42", "42"), Entry("float", "3.14", "3.14"), Entry("string", `"hello"`, `"hello"`), - Entry("raw string", "`hello`", "`hello`"), - Entry("multi-line raw string preserves newline", "`a\nb`", "`a\nb`"), - Entry("raw string with escaped backtick", "`say \\`hi\\``", "`say \\`hi\\``"), + Entry("raw string", `r"hello"`, `r"hello"`), + Entry("multi-line string preserves newline", "`a\nb`", "`a\nb`"), + Entry("format string", `f"hi {x}"`, `f"hi {x}"`), Entry("unit literal", "5ms", "5ms"), Entry("negated integer extracts inner literal", "-1", "1"), Entry("negated float extracts inner literal", "-3.14", "3.14"), @@ -108,9 +109,9 @@ var _ = Describe("AST Utilities", func() { DescribeTable("false cases", func(code string) { Expect(parser.IsNumericLiteral(parseExpr(code))).To(BeFalse()) }, Entry("string", `"hello"`), - Entry("raw string", "`hello`"), - Entry("multi-line raw string", "`a\nb`"), - Entry("raw string with escaped backtick", "`say \\`hi\\``"), + Entry("raw string", `r"hello"`), + Entry("multi-line string", "`a\nb`"), + Entry("format string", `f"hi {x}"`), Entry("identifier", "x"), Entry("addition", "1 + 2"), Entry("negated identifier", "-x"), @@ -133,8 +134,8 @@ var _ = Describe("AST Utilities", func() { Entry("string literal", `"hi"`, func(p parser.IPrimaryExpressionContext) { Expect(p.Literal().GetText()).To(Equal(`"hi"`)) }), - Entry("raw string literal", "`hi`", func(p parser.IPrimaryExpressionContext) { - Expect(p.Literal().GetText()).To(Equal("`hi`")) + Entry("format string literal", `f"hi {x}"`, func(p parser.IPrimaryExpressionContext) { + Expect(p.Literal().GetText()).To(Equal(`f"hi {x}"`)) }), ) diff --git a/arc/go/parser/parser_test.go b/arc/go/parser/parser_test.go index b207ccf4e9..fea37abbd1 100644 --- a/arc/go/parser/parser_test.go +++ b/arc/go/parser/parser_test.go @@ -953,26 +953,76 @@ func broken() { }) }) - Context("Raw String Literals", func() { - It("Should lex a multi-line raw string as a single STR_LITERAL_RAW token", func() { + Context("String Literals", func() { + It("Should lex a backtick string spanning newlines as a single STR_LITERAL_MULTI token", func() { expr := MustSucceed(parser.ParseExpression("`a\nb`")) lit := parser.GetLiteral(expr) Expect(lit).NotTo(BeNil()) - rawTok := lit.STR_LITERAL_RAW() - Expect(rawTok).NotTo(BeNil()) - Expect(rawTok.GetText()).To(Equal("`a\nb`")) + multiTok := lit.STR_LITERAL_MULTI() + Expect(multiTok).NotTo(BeNil()) + Expect(multiTok.GetText()).To(Equal("`a\nb`")) Expect(lit.STR_LITERAL()).To(BeNil()) }) - It("Should lex a raw string with escaped backticks as a single token", func() { - expr := MustSucceed(parser.ParseExpression("`say \\`hi\\``")) + It("Should lex an f-prefixed double-quoted string as STR_LITERAL", func() { + expr := MustSucceed(parser.ParseExpression(`f"hi {x}"`)) lit := parser.GetLiteral(expr) Expect(lit).NotTo(BeNil()) - rawTok := lit.STR_LITERAL_RAW() - Expect(rawTok).NotTo(BeNil()) - Expect(rawTok.GetText()).To(Equal("`say \\`hi\\``")) + tok := lit.STR_LITERAL() + Expect(tok).NotTo(BeNil()) + Expect(tok.GetText()).To(Equal(`f"hi {x}"`)) + Expect(lit.STR_LITERAL_MULTI()).To(BeNil()) + }) + + It("Should lex an rf-prefixed backtick string as STR_LITERAL_MULTI", func() { + expr := MustSucceed(parser.ParseExpression("rf`path: {p}\nraw: \\n`")) + lit := parser.GetLiteral(expr) + Expect(lit).NotTo(BeNil()) + multiTok := lit.STR_LITERAL_MULTI() + Expect(multiTok).NotTo(BeNil()) + Expect(multiTok.GetText()).To(Equal("rf`path: {p}\nraw: \\n`")) + Expect(lit.STR_LITERAL()).To(BeNil()) + }) + + It("Should lex a raw string with embedded escapes as a single STR_LITERAL", func() { + expr := MustSucceed(parser.ParseExpression(`r"say \"hi\""`)) + lit := parser.GetLiteral(expr) + Expect(lit).NotTo(BeNil()) + tok := lit.STR_LITERAL() + Expect(tok).NotTo(BeNil()) + Expect(tok.GetText()).To(Equal(`r"say \"hi\""`)) + Expect(lit.STR_LITERAL_MULTI()).To(BeNil()) + }) + + It("Should lex an f-prefixed backtick string as STR_LITERAL_MULTI", func() { + expr := MustSucceed(parser.ParseExpression("f`v={x}\nt={t}`")) + lit := parser.GetLiteral(expr) + Expect(lit).NotTo(BeNil()) + multiTok := lit.STR_LITERAL_MULTI() + Expect(multiTok).NotTo(BeNil()) + Expect(multiTok.GetText()).To(Equal("f`v={x}\nt={t}`")) Expect(lit.STR_LITERAL()).To(BeNil()) }) + + It("Should lex a backtick raw string as STR_LITERAL_MULTI", func() { + expr := MustSucceed(parser.ParseExpression("r`line1\nline2`")) + lit := parser.GetLiteral(expr) + Expect(lit).NotTo(BeNil()) + multiTok := lit.STR_LITERAL_MULTI() + Expect(multiTok).NotTo(BeNil()) + Expect(multiTok.GetText()).To(Equal("r`line1\nline2`")) + Expect(lit.STR_LITERAL()).To(BeNil()) + }) + + It("Should lex an rf-prefixed single-quoted string as STR_LITERAL", func() { + expr := MustSucceed(parser.ParseExpression(`rf"path: {p}"`)) + lit := parser.GetLiteral(expr) + Expect(lit).NotTo(BeNil()) + tok := lit.STR_LITERAL() + Expect(tok).NotTo(BeNil()) + Expect(tok.GetText()).To(Equal(`rf"path: {p}"`)) + Expect(lit.STR_LITERAL_MULTI()).To(BeNil()) + }) }) }) diff --git a/arc/go/stl/stl_test.go b/arc/go/stl/stl_test.go index d7cbe0edac..644532978f 100644 --- a/arc/go/stl/stl_test.go +++ b/arc/go/stl/stl_test.go @@ -76,6 +76,40 @@ var _ = Describe("SymbolResolver", func() { strings.Join(violations, "\n ")) }) + It("Should obey the ExecBoth structural contract on every ExecBoth symbol", func() { + var violations []string + for _, mod := range collectModuleResolvers(stl.SymbolResolver) { + for name, sym := range mod.Members { + if sym.Kind != symbol.KindFunction || sym.Exec != symbol.ExecBoth { + continue + } + inputs := sym.Type.Inputs + config := sym.Type.Config + if len(inputs) != len(config) { + violations = append(violations, fmt.Sprintf( + "%s.%s (Inputs has %d params, Config has %d; ExecBoth requires "+ + "one-to-one mirroring)", + mod.Name, name, len(inputs), len(config), + )) + continue + } + for i := range inputs { + if inputs[i].Name != config[i].Name || !types.Equal(inputs[i].Type, config[i].Type) { + violations = append(violations, fmt.Sprintf( + "%s.%s (Inputs[%d]={%s,%s} does not match Config[%d]={%s,%s})", + mod.Name, name, + i, inputs[i].Name, inputs[i].Type, + i, config[i].Name, config[i].Type, + )) + } + } + } + } + Expect(violations).To(BeEmpty(), + "ExecBoth symbols violating the dual-shape contract:\n "+ + strings.Join(violations, "\n ")) + }) + It("Should use DefaultOutputParam on user-callable single-output functions", func() { var violations []string for _, mod := range collectModuleResolvers(stl.SymbolResolver) { diff --git a/arc/go/stl/strings/string.go b/arc/go/stl/strings/string.go index 92b2634c81..b42b9cfd99 100644 --- a/arc/go/stl/strings/string.go +++ b/arc/go/stl/strings/string.go @@ -11,6 +11,7 @@ package strings import ( "context" + "fmt" "strconv" "github.com/synnaxlabs/arc/ir" @@ -63,69 +64,84 @@ var SymbolResolver = &symbol.ModuleResolver{ Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.I64()}}, }), }, - "from_i32": { - Name: "from_i32", - Kind: symbol.KindFunction, - Exec: symbol.ExecWASM, - Internal: true, - Type: types.Function(types.FunctionProperties{ - Inputs: types.Params{{Name: "value", Type: types.I32()}}, - Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, - }), - }, - "from_u32": { - Name: "from_u32", - Kind: symbol.KindFunction, - Exec: symbol.ExecWASM, - Internal: true, - Type: types.Function(types.FunctionProperties{ - Inputs: types.Params{{Name: "value", Type: types.U32()}}, - Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, - }), - }, - "from_i64": { - Name: "from_i64", - Kind: symbol.KindFunction, - Exec: symbol.ExecWASM, - Internal: true, - Type: types.Function(types.FunctionProperties{ - Inputs: types.Params{{Name: "value", Type: types.I64()}}, - Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, - }), - }, - "from_u64": { - Name: "from_u64", - Kind: symbol.KindFunction, - Exec: symbol.ExecWASM, - Internal: true, - Type: types.Function(types.FunctionProperties{ - Inputs: types.Params{{Name: "value", Type: types.U64()}}, - Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, - }), - }, - "from_f32": { - Name: "from_f32", - Kind: symbol.KindFunction, - Exec: symbol.ExecWASM, - Internal: true, - Type: types.Function(types.FunctionProperties{ - Inputs: types.Params{{Name: "value", Type: types.F32()}}, - Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, - }), - }, - "from_f64": { - Name: "from_f64", - Kind: symbol.KindFunction, - Exec: symbol.ExecWASM, - Internal: true, - Type: types.Function(types.FunctionProperties{ - Inputs: types.Params{{Name: "value", Type: types.F64()}}, - Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, - }), - }, + "from_i32": fromSym(types.I32()), + "from_u32": fromSym(types.U32()), + "from_i64": fromSym(types.I64()), + "from_u64": fromSym(types.U64()), + "from_f32": fromSym(types.F32()), + "from_f64": fromSym(types.F64()), + "format_i32": formatSym(types.I32()), + "format_u32": formatSym(types.U32()), + "format_i64": formatSym(types.I64()), + "format_u64": formatSym(types.U64()), + "format_f32": formatSym(types.F32()), + "format_f64": formatSym(types.F64()), + "format_string": formatSym(types.String()), }, } +func registerFrom[T any]( + builder wazero.HostModuleBuilder, + s *ProgramState, + name string, + conv func(T) string, +) wazero.HostModuleBuilder { + return builder.NewFunctionBuilder(). + WithFunc(func(_ context.Context, v T) uint32 { + return s.Create(conv(v)) + }).Export(name) +} + +func registerFormat[T any]( + builder wazero.HostModuleBuilder, + m *Module, + name string, + coerce func(T) any, +) wazero.HostModuleBuilder { + return builder.NewFunctionBuilder(). + WithFunc(func(_ context.Context, v T, ptr, length uint32) uint32 { + return m.strings.Create(formatWithSpec(m.memory, ptr, length, coerce(v))) + }).Export(name) +} + +func formatWithSpec(memory api.Memory, ptr, length uint32, value any) string { + spec, ok := memory.Read(ptr, length) + if !ok { + return "" + } + return fmt.Sprintf("%"+string(spec), value) +} + +func fromSym(value types.Type) symbol.Symbol { + return symbol.Symbol{ + Name: "from_" + value.String(), + Kind: symbol.KindFunction, + Exec: symbol.ExecWASM, + Internal: true, + Type: types.Function(types.FunctionProperties{ + Inputs: types.Params{{Name: "value", Type: value}}, + Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, + }), + } +} + +func formatSym(value types.Type) symbol.Symbol { + return symbol.Symbol{ + Name: "format_" + value.String(), + Kind: symbol.KindFunction, + Exec: symbol.ExecWASM, + Internal: true, + Type: types.Function(types.FunctionProperties{ + Inputs: types.Params{ + {Name: "value", Type: value}, + {Name: "spec_ptr", Type: types.I32()}, + {Name: "spec_len", Type: types.I32()}, + }, + Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: types.String()}}, + }), + } +} + type Module struct { strings *ProgramState memory api.Memory @@ -180,30 +196,26 @@ func NewModule( } return 0 }).Export("len") + builder = registerFrom(builder, s, "from_i32", func(v int32) string { return strconv.FormatInt(int64(v), 10) }) + builder = registerFrom(builder, s, "from_u32", func(v uint32) string { return strconv.FormatUint(uint64(v), 10) }) + builder = registerFrom(builder, s, "from_i64", func(v int64) string { return strconv.FormatInt(v, 10) }) + builder = registerFrom(builder, s, "from_u64", func(v uint64) string { return strconv.FormatUint(v, 10) }) + builder = registerFrom(builder, s, "from_f32", func(v float32) string { return strconv.FormatFloat(float64(v), 'g', -1, 32) }) + builder = registerFrom(builder, s, "from_f64", func(v float64) string { return strconv.FormatFloat(v, 'g', -1, 64) }) + builder = registerFormat(builder, m, "format_i32", func(v int32) any { return int64(v) }) + builder = registerFormat(builder, m, "format_u32", func(v uint32) any { return uint64(v) }) + builder = registerFormat(builder, m, "format_i64", func(v int64) any { return v }) + builder = registerFormat(builder, m, "format_u64", func(v uint64) any { return v }) + builder = registerFormat(builder, m, "format_f32", func(v float32) any { return float64(v) }) + builder = registerFormat(builder, m, "format_f64", func(v float64) any { return v }) builder = builder.NewFunctionBuilder(). - WithFunc(func(_ context.Context, v int32) uint32 { - return s.Create(strconv.FormatInt(int64(v), 10)) - }).Export("from_i32") - builder = builder.NewFunctionBuilder(). - WithFunc(func(_ context.Context, v uint32) uint32 { - return s.Create(strconv.FormatUint(uint64(v), 10)) - }).Export("from_u32") - builder = builder.NewFunctionBuilder(). - WithFunc(func(_ context.Context, v int64) uint32 { - return s.Create(strconv.FormatInt(v, 10)) - }).Export("from_i64") - builder = builder.NewFunctionBuilder(). - WithFunc(func(_ context.Context, v uint64) uint32 { - return s.Create(strconv.FormatUint(v, 10)) - }).Export("from_u64") - builder = builder.NewFunctionBuilder(). - WithFunc(func(_ context.Context, v float32) uint32 { - return s.Create(strconv.FormatFloat(float64(v), 'g', -1, 32)) - }).Export("from_f32") - builder = builder.NewFunctionBuilder(). - WithFunc(func(_ context.Context, v float64) uint32 { - return s.Create(strconv.FormatFloat(v, 'g', -1, 64)) - }).Export("from_f64") + WithFunc(func(_ context.Context, handle, ptr, length uint32) uint32 { + str, ok := s.Get(handle) + if !ok { + return 0 + } + return s.Create(formatWithSpec(m.memory, ptr, length, str)) + }).Export("format_string") if _, err := builder.Instantiate(ctx); err != nil { return nil, err } diff --git a/arc/go/stl/strings/string_test.go b/arc/go/stl/strings/string_test.go index a374f59df9..ad28a3a4ae 100644 --- a/arc/go/stl/strings/string_test.go +++ b/arc/go/stl/strings/string_test.go @@ -185,6 +185,7 @@ var _ = Describe("Strings", func() { }, Entry("simple decimal", float32(3.14), "3.14"), Entry("zero", float32(0.0), "0"), + Entry("negative zero", float32(math.Copysign(0, -1)), "-0"), Entry("negative", float32(-2.5), "-2.5"), Entry("1.0 (integer-valued)", float32(1.0), "1"), Entry("10.0 (integer-valued)", float32(10.0), "10"), @@ -221,6 +222,7 @@ var _ = Describe("Strings", func() { }, Entry("simple decimal", float64(3.14159), "3.14159"), Entry("zero", float64(0.0), "0"), + Entry("negative zero", math.Copysign(0, -1), "-0"), Entry("negative", float64(-2.5), "-2.5"), Entry("high precision", float64(0.1234567890123456), "0.1234567890123456"), Entry("1.0 (integer-valued)", float64(1.0), "1"), @@ -251,6 +253,81 @@ var _ = Describe("Strings", func() { }) }) + Describe("format_* spec read failures", func() { + writeSpec := func(spec string) (uint32, uint32) { + mem.Write(0, []byte(spec)) + return 0, uint32(len(spec)) + } + + It("Should format an i32 against a spec read from memory", func(ctx SpecContext) { + ptr, length := writeSpec("05d") + h := callU32(ctx, "format_i32", testutil.I32(7), testutil.U32(ptr), testutil.U32(length)) + Expect(MustBeOk(ss.Get(h))).To(Equal("00007")) + }) + + It("Should format an f64 against a spec read from memory", func(ctx SpecContext) { + ptr, length := writeSpec(".2f") + h := callU32(ctx, "format_f64", testutil.F64(3.14159), testutil.U32(ptr), testutil.U32(length)) + Expect(MustBeOk(ss.Get(h))).To(Equal("3.14")) + }) + + DescribeTable("Should return a handle to an empty string when the spec read is out-of-bounds", + func(ctx SpecContext, fn string, value uint64) { + h := callU32(ctx, fn, value, testutil.U32(1<<30), testutil.U32(4)) + Expect(h).ToNot(BeZero()) + Expect(MustBeOk(ss.Get(h))).To(BeEmpty()) + }, + Entry("format_i32 with OOB spec", "format_i32", testutil.I32(42)), + Entry("format_u32 with OOB spec", "format_u32", testutil.U32(42)), + Entry("format_i64 with OOB spec", "format_i64", testutil.I64(42)), + Entry("format_u64 with OOB spec", "format_u64", testutil.U64(42)), + Entry("format_f32 with OOB spec", "format_f32", testutil.F32(3.14)), + Entry("format_f64 with OOB spec", "format_f64", testutil.F64(3.14)), + ) + }) + + Describe("format_string", func() { + writeSpec := func(spec string) (uint32, uint32) { + mem.Write(0, []byte(spec)) + return 0, uint32(len(spec)) + } + + It("Should format a string handle against a spec read from memory", func(ctx SpecContext) { + h := ss.Create("hi") + ptr, length := writeSpec("5s") + rh := callU32(ctx, "format_string", testutil.U32(h), testutil.U32(ptr), testutil.U32(length)) + Expect(MustBeOk(ss.Get(rh))).To(Equal(" hi")) + }) + + It("Should return 0 for an unknown handle", func(ctx SpecContext) { + ptr, length := writeSpec("q") + rh := callU32(ctx, "format_string", testutil.U32(9999), testutil.U32(ptr), testutil.U32(length)) + Expect(rh).To(BeZero()) + }) + + It("Should return a handle to an empty string when the spec read is out-of-bounds", func(ctx SpecContext) { + h := ss.Create("hi") + rh := callU32(ctx, "format_string", testutil.U32(h), testutil.U32(1<<30), testutil.U32(4)) + Expect(rh).ToNot(BeZero()) + Expect(MustBeOk(ss.Get(rh))).To(BeEmpty()) + }) + }) + + Describe("Module.SetMemory", func() { + It("Should swap the backing memory used by format_*", func(ctx SpecContext) { + rt2 := testutil.NewRuntime(ctx) + defer func() { Expect(rt2.Close(ctx)).To(Succeed()) }() + ss2 := strings.NewProgramState() + m := MustSucceed(strings.NewModule(ctx, ss2, rt2.Underlying(), nil)) + rt2.Passthrough(ctx, "string") + mem2 := wazerotest.NewMemory(1) + m.SetMemory(mem2) + mem2.Write(0, []byte("05d")) + res := rt2.Call(ctx, "string", "format_i32", testutil.I32(7), testutil.U32(0), testutil.U32(3)) + Expect(MustBeOk(ss2.Get(testutil.AsU32(res[0])))).To(Equal("00007")) + }) + }) + Describe("cross-function handle reuse", func() { It("Should use from_literal result in concat and verify with equal", func(ctx SpecContext) { mem.Write(0, []byte("helloworld")) diff --git a/arc/go/str_cast_test.go b/arc/go/str_cast_test.go index c8bc5a1090..17e1183506 100644 --- a/arc/go/str_cast_test.go +++ b/arc/go/str_cast_test.go @@ -60,6 +60,8 @@ var _ = Describe("str() typecast end-to-end runtime", func() { Entry("float literal 123. (whole-valued, no fraction)", "str(123.)", "123"), Entry("float literal 1.0 (integer-valued)", "str(1.0)", "1"), Entry("float literal 100.000 (trailing zeros)", "str(100.000)", "100"), + Entry("float literal -0.0 (negative zero)", "str(-0.0)", "-0"), + Entry("float literal -0.0000 (negative zero with trailing zeros)", "str(-0.0000)", "-0"), Entry("explicit f32(3.14)", "str(f32(3.14))", "3.14"), Entry("explicit f64(3.14)", "str(f64(3.14))", "3.14"), Entry("integer literal 42", "str(42)", "42"), diff --git a/arc/go/symbol/symbol.go b/arc/go/symbol/symbol.go index 52bed884ae..2cdac709c3 100644 --- a/arc/go/symbol/symbol.go +++ b/arc/go/symbol/symbol.go @@ -65,7 +65,9 @@ const ( ExecWASM ExecContext = 1 << iota // ExecFlow marks a symbol as only usable in flow statements (graph nodes). ExecFlow - // ExecBoth marks a symbol as usable in both contexts. + // ExecBoth marks a symbol as usable in both contexts. Inputs must mirror + // Config one-for-one (N=0 allowed); upstream edges in flow form are + // triggers, not typed inputs. Invariant enforced in stl_test.go. ExecBoth = ExecWASM | ExecFlow ) diff --git a/arc/go/text/analyze.go b/arc/go/text/analyze.go index cbf606c47b..b2cd5a20fc 100644 --- a/arc/go/text/analyze.go +++ b/arc/go/text/analyze.go @@ -19,6 +19,7 @@ import ( "github.com/synnaxlabs/arc/analyzer" "github.com/synnaxlabs/arc/analyzer/authority" acontext "github.com/synnaxlabs/arc/analyzer/context" + "github.com/synnaxlabs/arc/compiler" "github.com/synnaxlabs/arc/ir" "github.com/synnaxlabs/arc/literal" "github.com/synnaxlabs/arc/parser" @@ -34,10 +35,11 @@ import ( // invocations with the same logical name receive distinct keys. type keyGenerator struct { occurrences map[string]int + synthFuncs *ir.Functions } -func newKeyGenerator() *keyGenerator { - return &keyGenerator{occurrences: make(map[string]int)} +func newKeyGenerator(synthFuncs *ir.Functions) *keyGenerator { + return &keyGenerator{occurrences: make(map[string]int), synthFuncs: synthFuncs} } func (kg *keyGenerator) generate(role, name string) string { @@ -488,6 +490,61 @@ func analyzeFunctionNode( return newNodeResult(n, firstInputParam(n.Inputs), firstOutputParam(n.Outputs)), true } +// tryAnalyzeFmtStrLiteral handles the format-string-with-placeholders case by +// emitting a synthetic function. handled=true means the literal was a format +// string with placeholders and the result is authoritative; handled=false +// means callers should fall through to other literal handling. +func tryAnalyzeFmtStrLiteral( + ctx acontext.Context[parser.IExpressionContext], + sym *symbol.Scope, + kg *keyGenerator, +) (nodeResult, bool, bool) { + literalCtx := parser.GetLiteral(ctx.AST) + if literalCtx == nil { + return nodeResult{}, false, false + } + strTerm := parser.StringTerminal(literalCtx) + if strTerm == nil { + return nodeResult{}, false, false + } + _, flags, ok := literal.StripQuotes(strTerm.GetText()) + if !ok || !flags.Format { + return nodeResult{}, false, false + } + outputType := ctx.Constraints.ApplySubstitutions(sym.Type.Outputs[0].Type) + parsedValue, err := literal.Parse(literalCtx, outputType) + if err != nil { + ctx.Diagnostics.Add(diagnostics.Error(err, ctx.AST)) + return nodeResult{}, true, false + } + body := parsedValue.Value.(string) + segments, err := literal.FmtStrParse(body) + if err != nil { + ctx.Diagnostics.Add(diagnostics.Error(err, ctx.AST)) + return nodeResult{}, true, false + } + if !literal.FmtStrHasPlaceholder(segments) { + return nodeResult{}, false, false + } + key := kg.generate("fmt", "") + synthKey := compiler.FmtStrSyntheticPrefix + key + *kg.synthFuncs = append(*kg.synthFuncs, ir.Function{ + Key: synthKey, + Body: ir.Body{Raw: body}, + Inputs: types.Params{}, + Config: types.Params{}, + Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: outputType}}, + Channels: sym.Channels.Copy(), + }) + n := ir.Node{ + Key: key, + Type: synthKey, + Channels: sym.Channels.Copy(), + Outputs: types.Params{{Name: ir.DefaultOutputParam, Type: outputType}}, + } + return newNodeResult(n, ir.DefaultInputParam, ir.DefaultOutputParam), true, true +} + func analyzeExpression( ctx acontext.Context[parser.IExpressionContext], kg *keyGenerator, @@ -498,6 +555,12 @@ func analyzeExpression( return nodeResult{}, false } + if sym.Kind == symbol.KindFunction && parser.IsLiteral(ctx.AST) { + if n, handled, ok := tryAnalyzeFmtStrLiteral(ctx, sym, kg); handled { + return n, ok + } + } + if sym.Kind == symbol.KindConstant { outputType := ctx.Constraints.ApplySubstitutions(sym.Type.Outputs[0].Type) literalCtx := parser.GetLiteral(ctx.AST) @@ -581,7 +644,7 @@ func Analyze( }) } } - kg := newKeyGenerator() + kg := newKeyGenerator(&i.Functions) shell := newShellBuilder() // The root scope is always parallel and always-live. diff --git a/arc/go/text/text_test.go b/arc/go/text/text_test.go index 340735d752..7836757b25 100644 --- a/arc/go/text/text_test.go +++ b/arc/go/text/text_test.go @@ -15,6 +15,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/samber/lo" + "github.com/synnaxlabs/arc/compiler" "github.com/synnaxlabs/arc/ir" "github.com/synnaxlabs/arc/stl" "github.com/synnaxlabs/arc/stl/authority" @@ -282,6 +283,26 @@ var _ = Describe("Text", func() { symbol.MapResolver{"output": {Name: "output", Kind: symbol.KindChannel, Type: types.Chan(types.F64()), ID: 10002}}, true, types.F64(), ), + Entry("string literal", + `"hello" -> output`, + symbol.MapResolver{"output": {Name: "output", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 10004}}, + true, types.String(), + ), + Entry("multi-line string literal", + "`hello\nworld` -> output", + symbol.MapResolver{"output": {Name: "output", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 10005}}, + true, types.String(), + ), + Entry("raw string literal", + `r"C:\path" -> output`, + symbol.MapResolver{"output": {Name: "output", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 10006}}, + true, types.String(), + ), + Entry("raw multi-line string literal", + "r`line1\\n\nline2` -> output", + symbol.MapResolver{"output": {Name: "output", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 10007}}, + true, types.String(), + ), Entry("complex expression (should not generate constant)", `1 + 2 -> output`, symbol.MapResolver{"output": {Name: "output", Kind: symbol.KindChannel, Type: types.Chan(types.I64()), ID: 10003}}, @@ -2453,6 +2474,146 @@ var _ = Describe("Text", func() { }) }) + Describe("Synthesized Format-String Functions", func() { + It("Registers a fmt$ function for a flow-form format string with a single placeholder", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F32()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := `sensor -> f"v={sensor}" -> log` + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + inter, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeTrue(), diagnostics.String()) + + synth := lo.Filter(inter.Functions, func(f ir.Function, _ int) bool { + return strings.HasPrefix(f.Key, compiler.FmtStrSyntheticPrefix) + }) + Expect(synth).To(HaveLen(1)) + f := synth[0] + Expect(f.Body.Raw).To(Equal("v={sensor}")) + Expect(f.Inputs).To(HaveLen(0)) + Expect(f.Config).To(HaveLen(0)) + Expect(f.Outputs).To(HaveLen(1)) + Expect(f.Outputs[0].Type).To(Equal(types.String())) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(100), "sensor")) + Expect(f.Channels.Write).To(BeEmpty()) + + synthNode := findNodeByType(inter.Nodes, f.Key) + Expect(synthNode.Channels.Read).To(HaveKeyWithValue(uint32(100), "sensor")) + }) + + It("Registers a fmt$ function whose Channels.Read covers every placeholder channel", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F32()), ID: 100}, + "t": {Name: "t", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 102}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := `sensor -> f"v={sensor} t={t}" -> log` + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + inter, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeTrue(), diagnostics.String()) + + synth := lo.Filter(inter.Functions, func(f ir.Function, _ int) bool { + return strings.HasPrefix(f.Key, compiler.FmtStrSyntheticPrefix) + }) + Expect(synth).To(HaveLen(1)) + f := synth[0] + Expect(f.Body.Raw).To(Equal("v={sensor} t={t}")) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(100), "sensor")) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(102), "t")) + }) + + It("Does not synthesize a fmt$ function for a literal format string with no placeholders", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "trig": {Name: "trig", Kind: symbol.KindChannel, Type: types.Chan(types.U8()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := `trig -> f"static" -> log` + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + inter, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeTrue(), diagnostics.String()) + + for _, f := range inter.Functions { + Expect(strings.HasPrefix(f.Key, compiler.FmtStrSyntheticPrefix)).To(BeFalse(), + "unexpected fmt$ synthetic %q for placeholder-free literal", f.Key) + } + }) + + It("Registers a fmt$ function for an rf-prefixed multi-line format string preserving backslashes across newlines", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F32()), ID: 100}, + "t": {Name: "t", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 102}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := "sensor -> rf`path\\to: {sensor}\nt={t}` -> log" + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + inter, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeTrue(), diagnostics.String()) + + synth := lo.Filter(inter.Functions, func(f ir.Function, _ int) bool { + return strings.HasPrefix(f.Key, compiler.FmtStrSyntheticPrefix) + }) + Expect(synth).To(HaveLen(1)) + f := synth[0] + Expect(f.Body.Raw).To(Equal("path\\to: {sensor}\nt={t}")) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(100), "sensor")) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(102), "t")) + }) + + It("Registers a fmt$ function for an rf-prefixed format string preserving backslashes", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F32()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := `sensor -> rf"path\to: {sensor}" -> log` + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + inter, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeTrue(), diagnostics.String()) + + synth := lo.Filter(inter.Functions, func(f ir.Function, _ int) bool { + return strings.HasPrefix(f.Key, compiler.FmtStrSyntheticPrefix) + }) + Expect(synth).To(HaveLen(1)) + f := synth[0] + Expect(f.Body.Raw).To(Equal(`path\to: {sensor}`)) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(100), "sensor")) + }) + + It("Registers a fmt$ function for a multi-line format string with placeholders across newlines", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F32()), ID: 100}, + "t": {Name: "t", Kind: symbol.KindChannel, Type: types.Chan(types.I32()), ID: 102}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := "sensor -> f`v={sensor}\nt={t}` -> log" + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + inter, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeTrue(), diagnostics.String()) + + synth := lo.Filter(inter.Functions, func(f ir.Function, _ int) bool { + return strings.HasPrefix(f.Key, compiler.FmtStrSyntheticPrefix) + }) + Expect(synth).To(HaveLen(1)) + f := synth[0] + Expect(f.Body.Raw).To(Equal("v={sensor}\nt={t}")) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(100), "sensor")) + Expect(f.Channels.Read).To(HaveKeyWithValue(uint32(102), "t")) + }) + + It("Surfaces analyzer diagnostics for an invalid format spec at this layer", func(ctx SpecContext) { + resolver := symbol.MapResolver{ + "sensor": {Name: "sensor", Kind: symbol.KindChannel, Type: types.Chan(types.F32()), ID: 100}, + "log": {Name: "log", Kind: symbol.KindChannel, Type: types.Chan(types.String()), ID: 101}, + } + source := `sensor -> f"v={sensor:d}" -> log` + parsedText := MustSucceed(text.Parse(text.Text{Raw: source})) + _, diagnostics := text.Analyze(ctx, parsedText, resolver) + Expect(diagnostics.Ok()).To(BeFalse(), + "expected an analyzer diagnostic for :d on a float channel") + Expect(diagnostics.String()).To(ContainSubstring("invalid format spec")) + }) + }) + Describe("Unit Dimensional Analysis", func() { DescribeTable("dimension compatibility", func(ctx SpecContext, source string, expectOk bool, expectedErrorContains string) { diff --git a/arc/ts/src/grammar/arc.tmLanguage.json b/arc/ts/src/grammar/arc.tmLanguage.json index 9d3d214638..25f4ac7d9b 100644 --- a/arc/ts/src/grammar/arc.tmLanguage.json +++ b/arc/ts/src/grammar/arc.tmLanguage.json @@ -123,35 +123,28 @@ "strings": { "patterns": [ { - "name": "string.quoted.raw.arc", - "begin": "`", - "end": "`", - "patterns": [ - { - "name": "constant.character.escape.arc", - "match": "\\\\`" - } - ] - }, - { - "name": "string.quoted.double.arc", - "begin": "\"", - "end": "\"", + "name": "string.quoted.arc", + "begin": "(rf|fr|f|r)?(\"|`)", + "end": "\\2", + "beginCaptures": { + "1": { "name": "storage.type.string.arc" } + }, "patterns": [ { "name": "constant.character.escape.arc", - "match": "\\\\([\\\\\"/bfnrt]|u[0-9a-fA-F]{4})" - } - ] - }, - { - "name": "string.quoted.single.arc", - "begin": "'", - "end": "'", - "patterns": [ + "match": "\\\\([\\\\\"`/bfnrt]|u[0-9a-fA-F]{4})" + }, { - "name": "constant.character.escape.arc", - "match": "\\\\([\\\\'bfnrt]|u[0-9a-fA-F]{4})" + "name": "meta.template.expression.arc", + "begin": "\\{", + "end": "\\}", + "beginCaptures": { + "0": { "name": "punctuation.definition.template-expression.begin.arc" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.template-expression.end.arc" } + }, + "patterns": [{ "include": "$self" }] } ] } diff --git a/console/src/arc/editor/external.ts b/console/src/arc/editor/external.ts index ebf95637f0..8f7078e6d6 100644 --- a/console/src/arc/editor/external.ts +++ b/console/src/arc/editor/external.ts @@ -9,4 +9,5 @@ export * from "@/arc/editor/CreateModal"; export * from "@/arc/editor/Editor"; +export * from "@/arc/editor/text"; export * from "@/arc/editor/Toolbar"; diff --git a/console/src/arc/editor/text/Editor.tsx b/console/src/arc/editor/text/Editor.tsx index 2918ec30cc..0ed924bae8 100644 --- a/console/src/arc/editor/text/Editor.tsx +++ b/console/src/arc/editor/text/Editor.tsx @@ -11,6 +11,7 @@ import { useCallback } from "react"; import { useDispatch } from "react-redux"; import { Controls } from "@/arc/editor/Controls"; +import { EXTENSIONS } from "@/arc/editor/text/placeholderSuggest"; import { useSelect } from "@/arc/selectors"; import { setRawText } from "@/arc/slice"; import { Editor as BaseEditor } from "@/code/Editor"; @@ -32,6 +33,7 @@ export const Editor: Layout.Renderer = ({ layoutKey }) => { onChange={onChange} language="arc" scrollBeyondLastLine + extensions={EXTENSIONS} /> diff --git a/console/src/arc/editor/text/external.ts b/console/src/arc/editor/text/external.ts index 89e52b45cb..0ec380ab02 100644 --- a/console/src/arc/editor/text/external.ts +++ b/console/src/arc/editor/text/external.ts @@ -8,4 +8,5 @@ // included in the file licenses/APL.txt. export * from "@/arc/editor/text/Editor"; +export * from "@/arc/editor/text/placeholderSuggest"; export * from "@/arc/editor/text/Toolbar"; diff --git a/console/src/arc/editor/text/placeholderSuggest.spec.ts b/console/src/arc/editor/text/placeholderSuggest.spec.ts new file mode 100644 index 0000000000..51f4b0d534 --- /dev/null +++ b/console/src/arc/editor/text/placeholderSuggest.spec.ts @@ -0,0 +1,146 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +import { describe, expect, it } from "vitest"; + +import { shouldTriggerSuggestion } from "@/arc/editor/text/placeholderSuggest"; + +interface TestCase { + name: string; + buffer: string; + typed: string; + expected: boolean; +} + +const TEST_CASES: TestCase[] = [ + { + name: "inside open f-string + open brace", + buffer: 'f"hello {na', + typed: "m", + expected: true, + }, + { + name: "after closed brace, no longer in placeholder", + buffer: 'f"hello {n}', + typed: "o", + expected: false, + }, + { + name: "inside f-string but no open brace", + buffer: 'f"hello ', + typed: "o", + expected: false, + }, + { + name: "nested open brace counts as placeholder", + buffer: 'f"a + {b + {c', + typed: "d", + expected: true, + }, + { + name: "typed char is `}` (not a word char)", + buffer: 'f"hello {x', + typed: "}", + expected: false, + }, + { name: "digit is a word char", buffer: 'f"hello {x', typed: "1", expected: true }, + { + name: "underscore is a word char", + buffer: 'f"hello {x', + typed: "_", + expected: true, + }, + { + name: "plain double-quoted string is not a format string", + buffer: '"{x', + typed: "y", + expected: false, + }, + { name: "empty buffer never matches", buffer: "", typed: "a", expected: false }, + { + name: "typed empty string never fires", + buffer: 'f"hello {x', + typed: "", + expected: false, + }, + { + name: "multi-char typed input never fires (paste)", + buffer: 'f"hello {x', + typed: "ab", + expected: false, + }, + { + name: "space is not a word char", + buffer: 'f"hello {x', + typed: " ", + expected: false, + }, + { + name: "caret on second line inside placeholder", + buffer: "x = f`hi\n{na", + typed: "m", + expected: true, + }, + { + name: "closed placeholder followed by new open one", + buffer: 'f"{a} {b', + typed: "c", + expected: true, + }, + { + name: "doubled open brace does not trigger", + buffer: 'f"hello {{va', + typed: "l", + expected: false, + }, + { + name: "real placeholder after doubled brace still triggers", + buffer: 'f"{{ {va', + typed: "l", + expected: true, + }, + { + name: "backslash adjacent to placeholder triggers", + buffer: 'rf"C:\\path\\{na', + typed: "m", + expected: true, + }, + { + name: "rf-prefixed string triggers", + buffer: 'rf"path: {va', + typed: "l", + expected: true, + }, + { + name: "fr-prefixed string triggers", + buffer: 'fr"path: {va', + typed: "l", + expected: true, + }, + { + name: "backtick f-string triggers", + buffer: "f`report:\n {va", + typed: "l", + expected: true, + }, + { + name: "r-prefixed (non-format) string does not trigger", + buffer: 'r"path {va', + typed: "l", + expected: false, + }, +]; + +describe("shouldTriggerSuggestion", () => { + TEST_CASES.forEach(({ name, buffer, typed, expected }) => + it(name, () => { + expect(shouldTriggerSuggestion(buffer, typed)).toBe(expected); + }), + ); +}); diff --git a/console/src/arc/editor/text/placeholderSuggest.ts b/console/src/arc/editor/text/placeholderSuggest.ts new file mode 100644 index 0000000000..36e13a265c --- /dev/null +++ b/console/src/arc/editor/text/placeholderSuggest.ts @@ -0,0 +1,38 @@ +// Copyright 2026 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +import { type Code } from "@/code"; + +// monaco-vscode-api ignores `tokenTypes` in language config, so the parent +// `string.quoted.*.arc` scope suppresses the popup inside `{...}`. Matches an +// f/rf/fr-prefixed string with an unclosed `{` at the caret; lookbehind and +// lookahead reject the `{` halves of a `{{` literal-brace escape. +const PLACEHOLDER_RE = /(?:rf|fr|f)["`](?:\\.|[^"`])*(? WORD_CHAR_RE.test(typedChar) && PLACEHOLDER_RE.test(bufferBeforeCaret); + +export const triggerSuggestInPlaceholders: Code.EditorExtension = (editor) => + editor.onDidChangeModelContent((e) => { + const ch = e.changes[0]; + if (e.changes.length !== 1 || ch.rangeLength !== 0) return; + const model = editor.getModel(); + const pos = editor.getPosition(); + if (model == null || pos == null) return; + const buffer = model.getValue().slice(0, model.getOffsetAt(pos)); + if (shouldTriggerSuggestion(buffer, ch.text)) + editor.trigger("arc.placeholderSuggest", "editor.action.triggerSuggest", { + auto: true, + }); + }); + +export const EXTENSIONS: Code.EditorExtension[] = [triggerSuggestInPlaceholders]; diff --git a/console/src/arc/lsp/lsp.ts b/console/src/arc/lsp/lsp.ts index 0e29a87cf1..a76c0e55bf 100644 --- a/console/src/arc/lsp/lsp.ts +++ b/console/src/arc/lsp/lsp.ts @@ -71,13 +71,12 @@ const TOKEN_CONFIG = { string: { dark: "#98C379", light: "#0A7D00", - scopes: ["string.quoted.double.arc", "string.quoted.single.arc"], + scopes: ["string.quoted.arc"], }, - // Distinct from `string` to mark verbatim, no-escape semantics. - stringRaw: { - dark: "#CE9178", - light: "#A31515", - scopes: ["string.quoted.raw.arc"], + stringPlaceholder: { + dark: "#CC255F", + light: "#CC255F", + scopes: [], }, number: { dark: "#98C379", @@ -106,6 +105,7 @@ const TOKEN_CONFIG = { "entity.name.function.arc", "support.function.builtin.arc", "support.function.arc", + "storage.type.string.arc", ], }, stage: { diff --git a/console/src/channel/Calculated.tsx b/console/src/channel/Calculated.tsx index d04ee90b21..85b6d87ab3 100644 --- a/console/src/channel/Calculated.tsx +++ b/console/src/channel/Calculated.tsx @@ -80,6 +80,7 @@ export const Calculated: Layout.Renderer = ({ layoutKey, onClose }): ReactElemen isBlock bordered rounded + extensions={Arc.Editor.Text.EXTENSIONS} /> )} diff --git a/console/src/code/Editor.tsx b/console/src/code/Editor.tsx index bb272ba3e6..de9c6cfb2b 100644 --- a/console/src/code/Editor.tsx +++ b/console/src/code/Editor.tsx @@ -96,11 +96,17 @@ const forwardGlobalTriggers = ( }; }; +// Plug-in point for attaching language-specific behavior to a Monaco editor. +export type EditorExtension = ( + editor: Monaco.editor.IStandaloneCodeEditor, +) => Monaco.IDisposable; + interface UseProps extends Input.Control { language: string; isBlock?: boolean; scrollBeyondLastLine?: boolean; openContextMenu?: Menu.ContextMenuProps["open"]; + extensions?: EditorExtension[]; } const useTheme = (language: string) => { @@ -122,6 +128,7 @@ const use = ({ isBlock = false, scrollBeyondLastLine = false, openContextMenu, + extensions, }: UseProps): UseReturn => { const containerRef = useRef(null); const editorRef = useRef(null); @@ -152,7 +159,7 @@ const use = ({ model = monaco.editor.createModel(value, language, uri); } - editorRef.current = monaco.editor.create(container, { + const editor = monaco.editor.create(container, { value: customURI != null ? undefined : value, model: model ?? undefined, language: customURI != null ? undefined : language, @@ -160,15 +167,15 @@ const use = ({ ...ZERO_OPTIONS, scrollBeyondLastLine, }); + editorRef.current = editor; disableMonacoCommandPalette(monaco); - const contentDispose = editorRef.current.onDidChangeModelContent(() => { - if (editorRef.current == null) return; - onChange(editorRef.current.getValue()); + const contentDispose = editor.onDidChangeModelContent(() => { + onChange(editor.getValue()); }); - const triggerDispose = forwardGlobalTriggers(editorRef.current); - const contextMenuDispose = editorRef.current.onContextMenu((e) => + const triggerDispose = forwardGlobalTriggers(editor); + const contextMenuDispose = editor.onContextMenu((e) => openContextMenuRef.current?.({ clientX: e.event.posx, clientY: e.event.posy, @@ -177,15 +184,17 @@ const use = ({ target: container, }), ); + const extensionDisposables = extensions?.map((ext) => ext(editor)) ?? []; return () => { contentDispose.dispose(); triggerDispose.dispose(); contextMenuDispose.dispose(); - editorRef.current?.dispose(); + extensionDisposables.forEach((d) => d.dispose()); + editor.dispose(); model?.dispose(); }; - }, [monaco, customURI]); + }, [monaco, customURI, extensions]); useEffect(() => { if (monaco == null) return; @@ -199,6 +208,7 @@ export interface EditorProps language: string; isBlock?: boolean; scrollBeyondLastLine?: boolean; + extensions?: EditorExtension[]; } const MENU_EDITOR_ACTIONS: Record = { @@ -216,6 +226,7 @@ export const Editor = ({ language, isBlock, scrollBeyondLastLine, + extensions, ...rest }: EditorProps) => { const { className: menuClassName, ...menuProps } = Menu.useContextMenu(); @@ -226,6 +237,7 @@ export const Editor = ({ isBlock, scrollBeyondLastLine, openContextMenu: menuProps.open, + extensions, }); const createMenuAction = useCallback( diff --git a/docs/site/src/pages/reference/control/arc/how-to/data-processing.mdx b/docs/site/src/pages/reference/control/arc/how-to/data-processing.mdx index 46c230c274..075eb2e9cf 100644 --- a/docs/site/src/pages/reference/control/arc/how-to/data-processing.mdx +++ b/docs/site/src/pages/reference/control/arc/how-to/data-processing.mdx @@ -390,6 +390,39 @@ output is in units per second (e.g., psi/second if pressure is in psi). +## Formatting Status Strings + +[Format strings](/reference/control/arc/reference/syntax#format-strings) build +human-readable status messages from live channel values. Placeholders inside `{...}` +accept any expression, and a `:spec` after the expression controls formatting: + +```arc +func fmt_pressure(value f64) str { + return f"Pressure: {value:.2f} psi" +} + +pressure -> fmt_pressure{} -> pressure_status +``` + +Each time `pressure` updates, `pressure_status` receives a string like +`"Pressure: 523.70 psi"`. Format specs are validated against the placeholder type at +compile time, so a `.2f` against an integer is caught before the program runs. + +Format strings are useful for writing diagnostic outputs: + +```arc +func alert(value f64) str { + if value > 800.0 { + return f"HIGH PRESSURE: {value:.1f} psi" + } + return f"pressure nominal: {value:.1f} psi" +} + +pressure -> alert{} -> log +``` + + + ## Putting It Together Here's a complete pipeline that reads a pressure sensor, converts units, smooths the diff --git a/docs/site/src/pages/reference/control/arc/reference/syntax.mdx b/docs/site/src/pages/reference/control/arc/reference/syntax.mdx index 65fcb2b192..e1126e5dd2 100644 --- a/docs/site/src/pages/reference/control/arc/reference/syntax.mdx +++ b/docs/site/src/pages/reference/control/arc/reference/syntax.mdx @@ -10,7 +10,8 @@ next: "Types" nextURL: "/reference/control/arc/reference/types" --- -import { Divider, Note } from "@synnaxlabs/pluto"; +import { Divider, Icon, Note, Text } from "@synnaxlabs/pluto"; +import Details from "@/components/details/Details.astro"; import { mdxOverrides } from "@/components/mdxOverrides"; export const components = mdxOverrides; @@ -105,22 +106,180 @@ parameter that expects a time span. ### String Literals -Strings are enclosed in double quotes. Escape sequences are supported. +Two forms, distinguished by delimiter: + +- `"..."` β€” single-line, double-quoted. Cannot contain unescaped newlines. +- `` `...` `` β€” multi-line, backtick-delimited. Newlines and `"` characters are allowed + verbatim in the body; the string ends at the next `` ` ``. ```arc "hello" -"line1\nline2" // newline -"tab\there" // tab -"quote: \"" // escaped quote -``` - -| Escape | Meaning | -| ------ | --------------- | -| `\n` | newline | -| `\t` | tab | -| `\r` | carriage return | -| `\\` | backslash | -| `\"` | double quote | +"tab\there" +`line1 +line2` +``` + +| Escape | Meaning | +| -------- | ------------------------------- | +| `\b` | backspace | +| `\t` | tab | +| `\n` | newline | +| `\f` | form feed | +| `\r` | carriage return | +| `\"` | double quote (single-line only) | +| `` \` `` | backtick (multi-line only) | +| `\\` | backslash | +| `\uXXXX` | 4-digit Unicode escape | + +The delimiter escape is asymmetric: `\"` is interpreted only inside `"..."`, and +`` \` `` only inside `` `...` ``. The other character is a literal everywhere it is not +the delimiter, so no escape is needed. Any other backslash sequence (e.g., `\z`) is +preserved as the two literal characters. + +### Raw Strings + +Prefix with `r` to disable escape processing. Every character in the body is verbatim β€” +`\n` is the two characters `\` and `n`, not a newline: + +```arc +r"C:\Users\path" +r`line one +line two` +``` + +In a raw string, the delimiter escape is still accepted by the lexer so the delimiter +character can appear in the body, but the backslash is preserved in the value (`r"a\"b"` +yields `a\"b`, four characters; `` r`a\`b` `` yields ``a\`b``). + +### Format Strings + +Prefix with `f` to enable `{expr}` placeholders, with an optional format spec after `:`. +Combine with `r` (as `rf` or `fr`) for raw + format. + +```arc +f"Data: {pressure:.1f} psi" // "Data: 523.7 psi" +f"Code: {pump_err_code:#x}" // "Code: 0xff00" +f"Math: {(2.0 + 4.0)/2.0}" // "Math: 3" +rf"C:\logs\{name}.txt" // backslashes literal; {name} substituted +``` + +Each placeholder is a regular Arc expression. Numeric values are converted to their +default string form unless a format spec is given. Format specs follow printf-style +conventions and are validated against the placeholder type at compile time. + +| Form | Meaning | +| ------------- | ----------------------------------------------------- | +| `{expr}` | Insert `expr` using its default string form | +| `{expr:spec}` | Format `expr` using a printf-style spec (e.g., `.2f`) | + +
+ Format spec reference + +A format spec has the form: + +``` +[flags][width][.precision]verb +``` + +Only the verb is required. Each bracketed part is optional. If `precision` is given, the +leading `.` must accompany it. Each part is described below. + +#### Verb + +The verb selects the conversion. Each verb accepts specific placeholder types. + +| Verb | Types | Use | Example | +| ---- | ----- | ----------------- | ----------------------- | +| `d` | int | Decimal | `42` β†’ `42` | +| `b` | int | Binary | `5` β†’ `101` | +| `o` | int | Octal | `8` β†’ `10` | +| `x` | int | Hex | `255` β†’ `ff` | +| `c` | int | Unicode character | `65` β†’ `A` | +| `f` | float | Decimal | `3.14` β†’ `3.140000` | +| `e` | float | Scientific | `3.14` β†’ `3.140000e+00` | +| `g` | float | Compact | `3.14` β†’ `3.14` | +| `s` | str | String | `hello` β†’ `hello` | +| `q` | str | Quoted string | `hello` β†’ `"hello"` | + +Verbs `x`, `e`, and `g` have uppercase variants `X`, `E`, and `G` that uppercase their +output (e.g., `X` of `255` β†’ `FF`). `O` is `o` with a leading `0o` prefix (e.g., `O` of +`8` β†’ `0o10`). + +`s` outputs the string as-is, identical to the default `{name}` form. Its practical use +is as the anchor verb when applying width or padding modifiers to a string: `{name:5s}` +pads to width 5, `{name:-10s}` left-aligns within width 10. See the Examples section +below. + +`q` wraps the string in double quotes and escapes special characters so the result is a +valid string literal: an embedded `"` becomes `\"`, a newline becomes `\n`, and so on. +For example, `q` of `say "hi"` produces `"say \"hi\""`. + +#### Flags + +Flags are zero or more of the following characters, in any order, placed at the start of +the spec. + +| Flag | Use | +| ---- | ----------------------------------------------------------------- | +| `#` | Alternate form: adds `0b` / `0` / `0x` prefix for `b` / `o` / `x` | +| `+` | Force sign on positive numbers | +| ` ` | Leading space for positive numbers | +| `-` | Left-align within width | +| `0` | Zero-pad to width | + +#### Width + +A digit count specifying the minimum output width. The value is padded with spaces (or +zeros, with the `0` flag) up to this width. For example, `5d` of `42` β†’ `␣␣␣42`. + +#### Precision + +A `.` followed by a digit count specifying digits after the decimal point. For example, +`.2f` of `3.14159` β†’ `3.14`. + +#### Examples + +Flags, width, and precision compose in spec order: + +| Spec | Value | Output | +| -------- | --------- | -------------- | +| `#b` | `5` | `0b101` | +| `#o` | `8` | `010` | +| `#x` | `255` | `0xff` | +| `X` | `255` | `FF` | +| `E` | `3.14` | `3.140000E+00` | +| `G` | `3.14` | `3.14` | +| `+d` | `42` | `+42` | +| ` d` | `42` | `␣42` | +| `5d` | `42` | `␣␣␣42` | +| `-5d` | `42` | `42␣␣␣` | +| `05d` | `42` | `00042` | +| `5s` | `"ok"` | `␣␣␣ok` | +| `-5s` | `"ok"` | `ok␣␣␣` | +| `.2f` | `3.14159` | `3.14` | +| `+f` | `3.14` | `+3.140000` | +| `6.2f` | `3.14` | `␣␣3.14` | +| `+08.2f` | `3.14` | `+0003.14` | +| `#06x` | `255` | `0x0000ff` | + +
+ +Inside a format string, `{` opens a placeholder. To write a literal `{`, double it as +`{{`; to write a literal `}`, double it as `}}`. A single `}` outside a placeholder is +treated as plain text. + +| Escape | Meaning | +| ------ | ----------- | +| `{{` | Literal `{` | +| `}}` | Literal `}` | + +```arc +msg := f"progress: {{{50}}}" // "progress: {50}" +``` + +The `{{` / `}}` escape mechanism is independent of the `r` prefix, so backslashes never +interfere with placeholder parsing. `rf"C:\logs\{name}.txt"` interpolates `{name}` and +keeps every backslash literal. ### Series Literals diff --git a/docs/site/src/pages/reference/control/arc/reference/types.mdx b/docs/site/src/pages/reference/control/arc/reference/types.mdx index 596b2c1a10..40d410dc0d 100644 --- a/docs/site/src/pages/reference/control/arc/reference/types.mdx +++ b/docs/site/src/pages/reference/control/arc/reference/types.mdx @@ -267,3 +267,7 @@ equal := msg == "Hello" // equality (returns u8: 1 or 0) | `[:]` (slicing) | Yes | | `len()` | Yes | | `<`, `>`, `<=`, `>=` | No | + +To build strings from runtime values, use +[format strings](/reference/control/arc/reference/syntax#format-strings) with `{expr}` +placeholders instead of repeated concatenation. diff --git a/integration/tests/arc/stl_string.py b/integration/tests/arc/stl_string.py index c782c097e4..6901dc5486 100644 --- a/integration/tests/arc/stl_string.py +++ b/integration/tests/arc/stl_string.py @@ -86,6 +86,72 @@ multi_add_out = len(s + str_second + "_suffix" + str_third) } str_trigger -> multi_add{} +// ──────────────────────── format strings (function) ─────────────────── +// One function exercises constants, local variables, and channel refs. +func fmt_fn() { + a := 99 + b := 1.5 + fmt_const_int_fn_out = f"int: {42}" + fmt_const_hex_fn_out = f"hex: {u8(255):x}" + fmt_const_float_fn_out = f"pi: {3.14159:.2f}" + fmt_var_int_fn_out = f"var int: {a}" + fmt_var_float_fn_out = f"var float: {b:.1f}" + fmt_var_expr_fn_out = f"expr: {a + 1}" + fmt_chan_int_fn_out = f"chan: {fmt_int_in}" + fmt_chan_float_fn_out = f"chan: {fmt_float_in:.2f}" + fmt_chan_str_fn_out = f"chan: {fmt_str_in:q}" + // Verb coverage: every numeric verb supported by the analyzer. + fmt_bin_fn_out = f"{5:b}" + fmt_oct_fn_out = f"{8:o}" + fmt_goct_fn_out = f"{8:O}" + fmt_hex_upper_fn_out = f"{u8(255):X}" + fmt_rune_ascii_fn_out = f"{u32(65):c}" + fmt_rune_utf8_fn_out = f"{u32(9731):c}" + fmt_sci_lower_fn_out = f"{1000000.0:e}" + fmt_sci_upper_fn_out = f"{1000000.0:E}" + fmt_short_fn_out = f"{3.14:g}" + // Alt flag (#): parity with Go's prefix-on-zero rule. + // %#x on 0 emits "0x0"; %#o on 0 emits "0" (no prefix). + fmt_alt_hex_zero_fn_out = f"{u8(0):#x}" + fmt_alt_oct_zero_fn_out = f"{u8(0):#o}" + fmt_alt_bin_fn_out = f"{5:#b}" + // Width, precision, sign flags. + fmt_width_fn_out = f"{42:5d}" + fmt_left_fn_out = f"{42:-5d}" + fmt_zero_pad_fn_out = f"{42:05d}" + fmt_plus_fn_out = f"{42:+d}" + fmt_prec_int_fn_out = f"{42:.4d}" + // Negative ints with non-decimal verbs: Go preserves the sign on the + // magnitude, unlike C printf which treats as unsigned. + fmt_neg_hex_fn_out = f"{-255:x}" + fmt_neg_alt_hex_fn_out = f"{-255:#x}" + fmt_neg_bin_fn_out = f"{-5:b}" + // String width and precision count UTF-8 runes, not bytes. + fmt_utf8_width_fn_out = f"{fmt_utf8_in:6s}" + fmt_utf8_prec_fn_out = f"{fmt_utf8_in:.3s}" + // Doubled-brace literal-brace escapes: {{ -> { and }} -> }. + fmt_brace_pair_fn_out = f"{{a}}" + fmt_brace_around_fn_out = f"{{{42}}}" + fmt_brace_path_fn_out = rf"C:\logs\{{abc}}.txt" + fmt_brace_path_int_fn_out = rf"C:\logs\{{{42}}}.txt" +} +fmt_trigger -> fmt_fn{} +// ──────────────────────── format strings (flow) ─────────────────────── +// Constants in flow position. +fmt_trigger -> f"int: {42}" -> fmt_const_int_flow_out +fmt_trigger -> f"hex: {u8(255):x}" -> fmt_const_hex_flow_out +fmt_trigger -> f"pi: {3.14159:.2f}" -> fmt_const_float_flow_out +// Channel references in flow position. +fmt_trigger -> f"chan: {fmt_int_in}" -> fmt_chan_int_flow_out +fmt_trigger -> f"chan: {fmt_float_in:.2f}" -> fmt_chan_float_flow_out +fmt_trigger -> f"chan: {fmt_str_in:q}" -> fmt_chan_str_flow_out +// Multiple placeholders in one flow expression. +fmt_trigger -> f"i={fmt_int_in}, f={fmt_float_in:.1f}" -> fmt_multi_flow_out +// Doubled-brace literal-brace escapes in flow position. This path is the +// one that historically dropped the {{/}} collapse and emitted the raw +// body verbatim; the assertions below pin the fixed behavior. +fmt_trigger -> rf"C:\logs\{{abc}}.txt" -> fmt_brace_path_flow_out +fmt_trigger -> rf"C:\logs\{42}.txt" -> fmt_brace_backslash_flow_out """ VIRTUAL_CHANNELS: list[tuple[str, sy.DataType]] = [ @@ -96,6 +162,62 @@ ("equal_xx_diff_out", sy.DataType.UINT8), ("concat_nested_out", sy.DataType.INT64), ("multi_add_out", sy.DataType.INT64), + # Format string inputs and outputs (all virtual to keep setup uniform). + ("fmt_trigger", sy.DataType.UINT8), + ("fmt_int_in", sy.DataType.INT64), + ("fmt_float_in", sy.DataType.FLOAT64), + ("fmt_str_in", sy.DataType.STRING), + ("fmt_utf8_in", sy.DataType.STRING), + ("fmt_const_int_fn_out", sy.DataType.STRING), + ("fmt_const_hex_fn_out", sy.DataType.STRING), + ("fmt_const_float_fn_out", sy.DataType.STRING), + ("fmt_var_int_fn_out", sy.DataType.STRING), + ("fmt_var_float_fn_out", sy.DataType.STRING), + ("fmt_var_expr_fn_out", sy.DataType.STRING), + ("fmt_chan_int_fn_out", sy.DataType.STRING), + ("fmt_chan_float_fn_out", sy.DataType.STRING), + ("fmt_chan_str_fn_out", sy.DataType.STRING), + # Verb coverage outputs. + ("fmt_bin_fn_out", sy.DataType.STRING), + ("fmt_oct_fn_out", sy.DataType.STRING), + ("fmt_goct_fn_out", sy.DataType.STRING), + ("fmt_hex_upper_fn_out", sy.DataType.STRING), + ("fmt_rune_ascii_fn_out", sy.DataType.STRING), + ("fmt_rune_utf8_fn_out", sy.DataType.STRING), + ("fmt_sci_lower_fn_out", sy.DataType.STRING), + ("fmt_sci_upper_fn_out", sy.DataType.STRING), + ("fmt_short_fn_out", sy.DataType.STRING), + # Alt-flag (#) parity outputs. + ("fmt_alt_hex_zero_fn_out", sy.DataType.STRING), + ("fmt_alt_oct_zero_fn_out", sy.DataType.STRING), + ("fmt_alt_bin_fn_out", sy.DataType.STRING), + # Width, precision, sign flags. + ("fmt_width_fn_out", sy.DataType.STRING), + ("fmt_left_fn_out", sy.DataType.STRING), + ("fmt_zero_pad_fn_out", sy.DataType.STRING), + ("fmt_plus_fn_out", sy.DataType.STRING), + ("fmt_prec_int_fn_out", sy.DataType.STRING), + # Negative non-decimal parity outputs. + ("fmt_neg_hex_fn_out", sy.DataType.STRING), + ("fmt_neg_alt_hex_fn_out", sy.DataType.STRING), + ("fmt_neg_bin_fn_out", sy.DataType.STRING), + # UTF-8 rune-count parity outputs. + ("fmt_utf8_width_fn_out", sy.DataType.STRING), + ("fmt_utf8_prec_fn_out", sy.DataType.STRING), + # Literal-brace escape ({{ / }}) outputs. + ("fmt_brace_pair_fn_out", sy.DataType.STRING), + ("fmt_brace_around_fn_out", sy.DataType.STRING), + ("fmt_brace_path_fn_out", sy.DataType.STRING), + ("fmt_brace_path_int_fn_out", sy.DataType.STRING), + ("fmt_brace_path_flow_out", sy.DataType.STRING), + ("fmt_brace_backslash_flow_out", sy.DataType.STRING), + ("fmt_const_int_flow_out", sy.DataType.STRING), + ("fmt_const_hex_flow_out", sy.DataType.STRING), + ("fmt_const_float_flow_out", sy.DataType.STRING), + ("fmt_chan_int_flow_out", sy.DataType.STRING), + ("fmt_chan_float_flow_out", sy.DataType.STRING), + ("fmt_chan_str_flow_out", sy.DataType.STRING), + ("fmt_multi_flow_out", sy.DataType.STRING), ] INDEXED_CHANNELS: list[tuple[str, sy.DataType]] = [ @@ -201,8 +323,150 @@ def _test_misc(self) -> None: self.log("[multi_add] Expecting 18 (len('helloother_suffix!'))") self.wait_for_eq("multi_add_out", 18, is_virtual=True) + def _test_format(self) -> None: + """Format strings in function and flow contexts. + + Format strings are a language feature (parser + compiler + runtime), + not part of the stl/strings module. They live here because they + produce string values and are closely related to the other string + operations exercised by this case; folding them in avoids the + overhead of a separate test case with its own arc, channels, and + teardown. + + Pre-writes input channels, then writes fmt_trigger to fire every flow + and the fmt_fn function in a single pass. Each output asserts the + end-to-end pipeline: parser, analyzer, compiler, runtime formatting. + """ + self.log("=== format strings ===") + self.writer.write("fmt_int_in", 42) + self.writer.write("fmt_float_in", 2.71828) + self.writer.write("fmt_str_in", "hello") + # 5 runes / 6 bytes β€” exercises Go's rune-count width and precision. + self.writer.write("fmt_utf8_in", "hΓ©llo") + self.writer.write("fmt_trigger", 1) + + # Function context: constants + self.log("[fmt_const_int_fn] Expecting 'int: 42'") + self.wait_for_eq("fmt_const_int_fn_out", "int: 42", is_virtual=True) + self.log("[fmt_const_hex_fn] Expecting 'hex: ff'") + self.wait_for_eq("fmt_const_hex_fn_out", "hex: ff", is_virtual=True) + self.log("[fmt_const_float_fn] Expecting 'pi: 3.14'") + self.wait_for_eq("fmt_const_float_fn_out", "pi: 3.14", is_virtual=True) + + # Function context: local variables + self.log("[fmt_var_int_fn] Expecting 'var int: 99'") + self.wait_for_eq("fmt_var_int_fn_out", "var int: 99", is_virtual=True) + self.log("[fmt_var_float_fn] Expecting 'var float: 1.5'") + self.wait_for_eq("fmt_var_float_fn_out", "var float: 1.5", is_virtual=True) + self.log("[fmt_var_expr_fn] Expecting 'expr: 100'") + self.wait_for_eq("fmt_var_expr_fn_out", "expr: 100", is_virtual=True) + + # Function context: channel references + self.log("[fmt_chan_int_fn] Expecting 'chan: 42'") + self.wait_for_eq("fmt_chan_int_fn_out", "chan: 42", is_virtual=True) + self.log("[fmt_chan_float_fn] Expecting 'chan: 2.72'") + self.wait_for_eq("fmt_chan_float_fn_out", "chan: 2.72", is_virtual=True) + self.log("[fmt_chan_str_fn] Expecting 'chan: \"hello\"'") + self.wait_for_eq("fmt_chan_str_fn_out", 'chan: "hello"', is_virtual=True) + + # Flow context: constants + self.log("[fmt_const_int_flow] Expecting 'int: 42'") + self.wait_for_eq("fmt_const_int_flow_out", "int: 42", is_virtual=True) + self.log("[fmt_const_hex_flow] Expecting 'hex: ff'") + self.wait_for_eq("fmt_const_hex_flow_out", "hex: ff", is_virtual=True) + self.log("[fmt_const_float_flow] Expecting 'pi: 3.14'") + self.wait_for_eq("fmt_const_float_flow_out", "pi: 3.14", is_virtual=True) + + # Flow context: channel references + self.log("[fmt_chan_int_flow] Expecting 'chan: 42'") + self.wait_for_eq("fmt_chan_int_flow_out", "chan: 42", is_virtual=True) + self.log("[fmt_chan_float_flow] Expecting 'chan: 2.72'") + self.wait_for_eq("fmt_chan_float_flow_out", "chan: 2.72", is_virtual=True) + self.log("[fmt_chan_str_flow] Expecting 'chan: \"hello\"'") + self.wait_for_eq("fmt_chan_str_flow_out", 'chan: "hello"', is_virtual=True) + + # Flow context: multiple placeholders + self.log("[fmt_multi_flow] Expecting 'i=42, f=2.7'") + self.wait_for_eq("fmt_multi_flow_out", "i=42, f=2.7", is_virtual=True) + + # Verb coverage: one case per verb supported by the analyzer. + self.log("[fmt_bin] Expecting '101'") + self.wait_for_eq("fmt_bin_fn_out", "101", is_virtual=True) + self.log("[fmt_oct] Expecting '10'") + self.wait_for_eq("fmt_oct_fn_out", "10", is_virtual=True) + self.log("[fmt_goct] Expecting '0o10'") + self.wait_for_eq("fmt_goct_fn_out", "0o10", is_virtual=True) + self.log("[fmt_hex_upper] Expecting 'FF'") + self.wait_for_eq("fmt_hex_upper_fn_out", "FF", is_virtual=True) + self.log("[fmt_rune_ascii] Expecting 'A'") + self.wait_for_eq("fmt_rune_ascii_fn_out", "A", is_virtual=True) + self.log("[fmt_rune_utf8] Expecting 'β˜ƒ'") + self.wait_for_eq("fmt_rune_utf8_fn_out", "β˜ƒ", is_virtual=True) + self.log("[fmt_sci_lower] Expecting '1.000000e+06'") + self.wait_for_eq("fmt_sci_lower_fn_out", "1.000000e+06", is_virtual=True) + self.log("[fmt_sci_upper] Expecting '1.000000E+06'") + self.wait_for_eq("fmt_sci_upper_fn_out", "1.000000E+06", is_virtual=True) + self.log("[fmt_short] Expecting '3.14'") + self.wait_for_eq("fmt_short_fn_out", "3.14", is_virtual=True) + + # Alt flag (#) on zero: Go emits "0x0"/"0b0" but suppresses for octal. + self.log("[fmt_alt_hex_zero] Expecting '0x0'") + self.wait_for_eq("fmt_alt_hex_zero_fn_out", "0x0", is_virtual=True) + self.log("[fmt_alt_oct_zero] Expecting '0'") + self.wait_for_eq("fmt_alt_oct_zero_fn_out", "0", is_virtual=True) + self.log("[fmt_alt_bin] Expecting '0b101'") + self.wait_for_eq("fmt_alt_bin_fn_out", "0b101", is_virtual=True) + + # Width, precision, sign flags. + self.log("[fmt_width] Expecting ' 42'") + self.wait_for_eq("fmt_width_fn_out", " 42", is_virtual=True) + self.log("[fmt_left] Expecting '42 '") + self.wait_for_eq("fmt_left_fn_out", "42 ", is_virtual=True) + self.log("[fmt_zero_pad] Expecting '00042'") + self.wait_for_eq("fmt_zero_pad_fn_out", "00042", is_virtual=True) + self.log("[fmt_plus] Expecting '+42'") + self.wait_for_eq("fmt_plus_fn_out", "+42", is_virtual=True) + self.log("[fmt_prec_int] Expecting '0042'") + self.wait_for_eq("fmt_prec_int_fn_out", "0042", is_virtual=True) + + # Negative ints with non-decimal verbs: sign preserved per Go. + self.log("[fmt_neg_hex] Expecting '-ff'") + self.wait_for_eq("fmt_neg_hex_fn_out", "-ff", is_virtual=True) + self.log("[fmt_neg_alt_hex] Expecting '-0xff'") + self.wait_for_eq("fmt_neg_alt_hex_fn_out", "-0xff", is_virtual=True) + self.log("[fmt_neg_bin] Expecting '-101'") + self.wait_for_eq("fmt_neg_bin_fn_out", "-101", is_virtual=True) + + # UTF-8 width and precision: Go counts code points, not bytes. + # "hΓ©llo" is 5 runes / 6 bytes; %6s pads to 6 runes, %.3s keeps 3. + self.log("[fmt_utf8_width] Expecting ' hΓ©llo'") + self.wait_for_eq("fmt_utf8_width_fn_out", " hΓ©llo", is_virtual=True) + self.log("[fmt_utf8_prec] Expecting 'hΓ©l'") + self.wait_for_eq("fmt_utf8_prec_fn_out", "hΓ©l", is_virtual=True) + + # Literal-brace escapes: {{ -> { and }} -> }. + self.log("[fmt_brace_pair] Expecting '{a}'") + self.wait_for_eq("fmt_brace_pair_fn_out", "{a}", is_virtual=True) + self.log("[fmt_brace_around] Expecting '{42}'") + self.wait_for_eq("fmt_brace_around_fn_out", "{42}", is_virtual=True) + self.log("[fmt_brace_path] Expecting 'C:\\logs\\{abc}.txt'") + self.wait_for_eq("fmt_brace_path_fn_out", r"C:\logs\{abc}.txt", is_virtual=True) + self.log("[fmt_brace_path_int] Expecting 'C:\\logs\\{42}.txt'") + self.wait_for_eq( + "fmt_brace_path_int_fn_out", r"C:\logs\{42}.txt", is_virtual=True + ) + self.log("[fmt_brace_path_flow] Expecting 'C:\\logs\\{abc}.txt'") + self.wait_for_eq( + "fmt_brace_path_flow_out", r"C:\logs\{abc}.txt", is_virtual=True + ) + self.log("[fmt_brace_backslash_flow] Expecting 'C:\\logs\\42.txt'") + self.wait_for_eq( + "fmt_brace_backslash_flow_out", r"C:\logs\42.txt", is_virtual=True + ) + def verify_sequence_execution(self) -> None: self._test_len() self._test_concat() self._test_equal() self._test_misc() + self._test_format() diff --git a/x/go/diagnostics/diagnostics.go b/x/go/diagnostics/diagnostics.go index 2aef14fd51..c482e0f62d 100644 --- a/x/go/diagnostics/diagnostics.go +++ b/x/go/diagnostics/diagnostics.go @@ -25,6 +25,20 @@ type Position struct { Col int } +// Advance returns the position reached by walking off bytes of body from p, +// resetting Col on each newline. +func (p Position) Advance(body string, off int) Position { + for i := 0; i < off && i < len(body); i++ { + if body[i] == '\n' { + p.Line++ + p.Col = 0 + } else { + p.Col++ + } + } + return p +} + //go:generate stringer -type=Severity const ( // SeverityError indicates a critical issue that prevents compilation. @@ -139,6 +153,13 @@ func (d Diagnostic) WithCode(code ErrorCode) Diagnostic { return d } +// WithRange returns a copy of the diagnostic with explicit Start and End +// positions, overriding any range set by SetRange. +func (d Diagnostic) WithRange(start, end Position) Diagnostic { + d.Start, d.End = start, end + return d +} + // WithNote returns a copy of the diagnostic with an additional note. func (d Diagnostic) WithNote(note string) Diagnostic { if note != "" { diff --git a/x/go/diagnostics/diagnostics_test.go b/x/go/diagnostics/diagnostics_test.go index a9fc27797e..d8212d91a4 100644 --- a/x/go/diagnostics/diagnostics_test.go +++ b/x/go/diagnostics/diagnostics_test.go @@ -431,4 +431,93 @@ var _ = Describe("Diagnostics", func() { Expect(d[0].Notes).To(BeEmpty()) }) }) + + Describe("Position.Advance", func() { + DescribeTable("Should walk body bytes and reset column on newlines", + func(body string, start diagnostics.Position, off int, expected diagnostics.Position) { + Expect(start.Advance(body, off)).To(Equal(expected)) + }, + Entry("zero offset returns start unchanged", + "abc", diagnostics.Position{Line: 1, Col: 1}, 0, + diagnostics.Position{Line: 1, Col: 1}), + Entry("walks N non-newline bytes incrementing Col", + "abc", diagnostics.Position{Line: 1, Col: 1}, 2, + diagnostics.Position{Line: 1, Col: 3}), + Entry("newline bumps Line and resets Col to 0", + "a\nb", diagnostics.Position{Line: 1, Col: 1}, 2, + diagnostics.Position{Line: 2, Col: 0}), + Entry("consecutive newlines each reset Col", + "\n\n", diagnostics.Position{Line: 1, Col: 1}, 2, + diagnostics.Position{Line: 3, Col: 0}), + Entry("mixed text and newline", + "abc\ndef", diagnostics.Position{Line: 1, Col: 1}, 6, + diagnostics.Position{Line: 2, Col: 2}), + Entry("offset past len(body) clamps at end of body", + "abc", diagnostics.Position{Line: 1, Col: 1}, 100, + diagnostics.Position{Line: 1, Col: 4}), + Entry("empty body returns start unchanged", + "", diagnostics.Position{Line: 5, Col: 7}, 0, + diagnostics.Position{Line: 5, Col: 7}), + Entry("empty body with non-zero offset still returns start", + "", diagnostics.Position{Line: 5, Col: 7}, 10, + diagnostics.Position{Line: 5, Col: 7}), + Entry("trailing newline lands on next line at col 0", + "abc\n", diagnostics.Position{Line: 1, Col: 1}, 4, + diagnostics.Position{Line: 2, Col: 0}), + Entry("starts on a non-zero line and column", + "xy", diagnostics.Position{Line: 7, Col: 3}, 2, + diagnostics.Position{Line: 7, Col: 5}), + ) + + It("Should not mutate the receiver", func() { + start := diagnostics.Position{Line: 1, Col: 1} + _ = start.Advance("a\nb", 3) + Expect(start).To(Equal(diagnostics.Position{Line: 1, Col: 1})) + }) + }) + + Describe("Diagnostic.WithRange", func() { + It("Should override Start and End regardless of prior values", func() { + d := diagnostics.Diagnostic{ + Start: diagnostics.Position{Line: 1, Col: 0}, + End: diagnostics.Position{Line: 1, Col: 5}, + } + out := d.WithRange( + diagnostics.Position{Line: 3, Col: 2}, + diagnostics.Position{Line: 3, Col: 8}, + ) + Expect(out.Start).To(Equal(diagnostics.Position{Line: 3, Col: 2})) + Expect(out.End).To(Equal(diagnostics.Position{Line: 3, Col: 8})) + }) + + It("Should return a copy and leave the original unchanged", func() { + d := diagnostics.Diagnostic{ + Start: diagnostics.Position{Line: 1, Col: 0}, + End: diagnostics.Position{Line: 1, Col: 5}, + } + _ = d.WithRange( + diagnostics.Position{Line: 9, Col: 9}, + diagnostics.Position{Line: 9, Col: 9}, + ) + Expect(d.Start).To(Equal(diagnostics.Position{Line: 1, Col: 0})) + Expect(d.End).To(Equal(diagnostics.Position{Line: 1, Col: 5})) + }) + + It("Should preserve unrelated fields", func() { + d := diagnostics.Diagnostic{ + Severity: diagnostics.SeverityWarning, + Message: "msg", + Code: "C001", + Notes: []diagnostics.Note{{Message: "n"}}, + } + out := d.WithRange( + diagnostics.Position{Line: 2, Col: 0}, + diagnostics.Position{Line: 2, Col: 4}, + ) + Expect(out.Severity).To(Equal(diagnostics.SeverityWarning)) + Expect(out.Message).To(Equal("msg")) + Expect(out.Code).To(Equal(diagnostics.ErrorCode("C001"))) + Expect(out.Notes).To(HaveLen(1)) + }) + }) })