diff options
-rw-r--r-- | src/ast/fwd.hpp | 27 | ||||
-rw-r--r-- | src/ast/npc.cpp | 594 | ||||
-rw-r--r-- | src/ast/npc.hpp | 135 | ||||
-rw-r--r-- | src/ast/npc_test.cpp | 569 | ||||
-rw-r--r-- | src/ast/script.cpp | 71 | ||||
-rw-r--r-- | src/ast/script.hpp | 94 | ||||
-rw-r--r-- | src/compat/result.hpp | 78 | ||||
-rw-r--r-- | src/compat/result_test.cpp | 79 | ||||
-rw-r--r-- | src/io/line.hpp | 13 | ||||
-rw-r--r-- | src/io/line_test.cpp | 16 | ||||
-rw-r--r-- | src/io/read.cpp | 2 | ||||
-rw-r--r-- | src/io/read_test.cpp | 6 | ||||
-rw-r--r-- | src/io/write.cpp | 2 | ||||
-rw-r--r-- | src/map/clif.cpp | 34 | ||||
-rw-r--r-- | src/map/clif.hpp | 2 | ||||
-rw-r--r-- | src/mmo/extract.cpp | 179 | ||||
-rw-r--r-- | src/mmo/extract.hpp | 11 | ||||
-rw-r--r-- | src/mmo/extract_test.cpp | 88 | ||||
-rw-r--r-- | src/sexpr/lexer_test.cpp | 10 | ||||
-rw-r--r-- | src/sexpr/parser_test.cpp | 3 | ||||
-rw-r--r-- | src/strings/rstring.cpp | 19 | ||||
-rw-r--r-- | src/strings/strings2_test.cpp | 9 |
22 files changed, 2025 insertions, 16 deletions
diff --git a/src/ast/fwd.hpp b/src/ast/fwd.hpp new file mode 100644 index 0000000..77328c9 --- /dev/null +++ b/src/ast/fwd.hpp @@ -0,0 +1,27 @@ +#pragma once +// ast/fwd.hpp - list of type names for new independent tmwa ast +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "../sanity.hpp" + + +namespace tmwa +{ +// meh, add more when I feel like it +} // namespace tmwa diff --git a/src/ast/npc.cpp b/src/ast/npc.cpp new file mode 100644 index 0000000..362943c --- /dev/null +++ b/src/ast/npc.cpp @@ -0,0 +1,594 @@ +#include "npc.hpp" +// ast/npc.cpp - Structure of non-player characters (including mobs). +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "../io/cxxstdio.hpp" + +#include "../mmo/extract.hpp" + +#include "../map/clif.hpp" + +#include "../poison.hpp" + + +namespace tmwa +{ +namespace npc +{ +namespace parse +{ + // separate file because virtual + TopLevel::~TopLevel() {} + +#define TRY_EXTRACT(bit, var) ({ if (!extract(bit.data, &var.data)) return Err(bit.span.error_str("failed to extract"_s #var)); var.span = bit.span; }) + + static + Result<Warp> parse_warp(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 3) + { + return Err(bits[0].span.error_str("expect 3 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "warp"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 5) + { + return Err(bits[3].span.error_str("expect 5 ,component,s"_s)); + } + + Warp warp; + warp.span = span; + TRY_EXTRACT(bits[0].data[0], warp.m); + TRY_EXTRACT(bits[0].data[1], warp.x); + TRY_EXTRACT(bits[0].data[2], warp.y); + warp.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], warp.name); + TRY_EXTRACT(bits[3].data[0], warp.xs); + TRY_EXTRACT(bits[3].data[1], warp.ys); + TRY_EXTRACT(bits[3].data[2], warp.to_m); + TRY_EXTRACT(bits[3].data[3], warp.to_x); + TRY_EXTRACT(bits[3].data[4], warp.to_y); + return Ok(std::move(warp)); + } + static + Result<Shop> parse_shop(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 4) + { + return Err(bits[0].span.error_str("expect 4 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "shop"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() < 2) + { + return Err(bits[3].span.error_str("expect at least 2 ,component,s"_s)); + } + + Shop shop; + shop.span = span; + TRY_EXTRACT(bits[0].data[0], shop.m); + TRY_EXTRACT(bits[0].data[1], shop.x); + TRY_EXTRACT(bits[0].data[2], shop.y); + TRY_EXTRACT(bits[0].data[3], shop.d); + shop.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], shop.name); + TRY_EXTRACT(bits[3].data[0], shop.npc_class); + shop.items.span = bits[3].span; + shop.items.span.begin = bits[3].data[1].span.begin; + for (size_t i = 1; i < bits[3].data.size(); ++i) + { + shop.items.data.emplace_back(); + auto& item = shop.items.data.back(); + auto& data = bits[3].data[i]; + assert(data.span.begin.line == data.span.end.line); + item.span = data.span; + + XString name; + if (!extract(data.data, record<':'>(&name, &item.data.value.data))) + return Err(data.span.error_str("Failed to split item:value"_s)); + item.data.name.span = item.span; + item.data.name.span.end.column = item.data.name.span.begin.column + name.size() - 1; + item.data.value.span = item.span; + item.data.value.span.begin.column = item.data.name.span.begin.column + name.size() + 1; + if (name.endswith(' ')) + { + item.data.name.span.warning("Shop item has useless space before the colon"_s); + name = name.rstrip(); + } + if (name.is_digit10()) + { + item.data.name.span.warning("Shop item is an id; should be a name"_s); + } + if (!extract(name, &item.data.name.data)) + return Err("item name problem (too long?)"_s); + } + return Ok(std::move(shop)); + } + static + Result<Monster> parse_monster(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 3 && bits[0].data.size() != 5) + { + return Err(bits[0].span.error_str("expect 3 or 5 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "monster"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 2 && bits[3].data.size() != 4 && bits[3].data.size() != 5) + { + return Err(bits[3].span.error_str("expect 2, 4, or 5 ,component,s"_s)); + } + + Monster mob; + mob.span = span; + TRY_EXTRACT(bits[0].data[0], mob.m); + TRY_EXTRACT(bits[0].data[1], mob.x); + TRY_EXTRACT(bits[0].data[2], mob.y); + if (bits[0].data.size() >= 5) + { + TRY_EXTRACT(bits[0].data[3], mob.xs); + TRY_EXTRACT(bits[0].data[4], mob.ys); + } + else + { + mob.xs.data = 0; + mob.xs.span = bits[0].data[2].span; + mob.xs.span.end.column++; + mob.xs.span.begin.column = mob.xs.span.end.column; + mob.ys.data = 0; + mob.ys.span = mob.xs.span; + } + mob.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], mob.name); + TRY_EXTRACT(bits[3].data[0], mob.mob_class); + TRY_EXTRACT(bits[3].data[1], mob.num); + if (bits[3].data.size() >= 4) + { + static_assert(std::is_same<decltype(mob.delay1.data)::period, std::chrono::milliseconds::period>::value, "delay1 is milliseconds"); + static_assert(std::is_same<decltype(mob.delay2.data)::period, std::chrono::milliseconds::period>::value, "delay2 is milliseconds"); + if (bits[3].data[2].data.is_digit10()) + bits[3].data[2].span.warning("delay1 lacks units; defaulting to ms"_s); + if (bits[3].data[3].data.is_digit10()) + bits[3].data[3].span.warning("delay2 lacks units; defaulting to ms"_s); + TRY_EXTRACT(bits[3].data[2], mob.delay1); + TRY_EXTRACT(bits[3].data[3], mob.delay2); + if (bits[3].data.size() >= 5) + { + TRY_EXTRACT(bits[3].data[4], mob.event); + } + else + { + mob.event.data = NpcEvent(); + mob.event.span = bits[3].data[3].span; + mob.event.span.end.column++; + mob.event.span.begin.column = mob.event.span.end.column; + } + } + else + { + mob.delay1.data = std::chrono::milliseconds::zero(); + mob.delay1.span = bits[3].data[1].span; + mob.delay1.span.end.column++; + mob.delay1.span.begin.column = mob.delay1.span.end.column; + mob.delay2.data = std::chrono::milliseconds::zero(); + mob.delay2.span = mob.delay1.span; + mob.event.data = NpcEvent(); + mob.event.span = mob.delay1.span; + } + return Ok(std::move(mob)); + } + static + Result<MapFlag> parse_mapflag(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits) + { + if (bits.size() != 3 && bits.size() != 4) + { + return Err(span.error_str("expect 3 or 4 |component|s"_s)); + } + if (bits[0].data.size() != 1) + { + return Err(bits[0].span.error_str("expect 1 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "mapflag"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits.size() >= 4) + { + if (bits[3].data.size() != 1) + { + return Err(bits[3].span.error_str("expect 1 ,component,s"_s)); + } + } + + MapFlag mapflag; + mapflag.span = span; + TRY_EXTRACT(bits[0].data[0], mapflag.m); + mapflag.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], mapflag.name); + if (bits.size() >= 4) + { + TRY_EXTRACT(bits[3].data[0], mapflag.opt_extra); + } + else + { + mapflag.opt_extra.data = ""_s; + mapflag.opt_extra.span = bits[2].span; + mapflag.opt_extra.span.end.column++; + mapflag.opt_extra.span.begin.column = mapflag.opt_extra.span.end.column; + } + return Ok(std::move(mapflag)); + } + static + Result<ScriptFunction> parse_script_function(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptFunction: function|script|Fun Name{code} + if (bits.size() != 3) + { + return Err(span.error_str("expect 3 |component|s"_s)); + } + assert(bits[0].data.size() == 1); + assert(bits[0].data[0].data == "function"_s); + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + + ScriptFunction script_function; + script_function.span = span; + script_function.key1_span = bits[0].data[0].span; + script_function.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_function.name); + // also expect '{' and parse real script + script_function.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_function)); + } + static + Result<ScriptNone> parse_script_none(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptNone: -|script|script name|-1{code} + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + assert(bits[0].data.size() == 1); + assert(bits[0].data[0].data == "-"_s); + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + assert(bits[3].data[0].data == "-1"_s); + if (bits[3].data.size() != 1) + { + return Err(bits[2].span.error_str("last |component| should be just -1"_s)); + } + + ScriptNone script_none; + script_none.span = span; + script_none.key1_span = bits[0].data[0].span; + script_none.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_none.name); + script_none.key4_span = bits[3].data[0].span; + // also expect '{' and parse real script + script_none.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_none)); + } + static + Result<ScriptMapNone> parse_script_map_none(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptMapNone: m,x,y,d|script|script name|-1{code} + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 4) + { + return Err(bits[0].span.error_str("expect 3 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 1 || bits[3].data[0].data != "-1"_s) + { + return Err(bits[2].span.error_str("last |component| should be just -1"_s)); + } + + ScriptMapNone script_map_none; + script_map_none.span = span; + TRY_EXTRACT(bits[0].data[0], script_map_none.m); + TRY_EXTRACT(bits[0].data[1], script_map_none.x); + TRY_EXTRACT(bits[0].data[2], script_map_none.y); + TRY_EXTRACT(bits[0].data[3], script_map_none.d); + script_map_none.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_map_none.name); + script_map_none.key4_span = bits[3].data[0].span; + // also expect '{' and parse real script + script_map_none.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_map_none)); + } + static + Result<ScriptMap> parse_script_map(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + // ScriptMap: m,x,y,d|script|script name|class,xs,ys{code} + if (bits.size() != 4) + { + return Err(span.error_str("expect 4 |component|s"_s)); + } + if (bits[0].data.size() != 4) + { + return Err(bits[0].span.error_str("expect 3 ,component,s"_s)); + } + assert(bits[1].data.size() == 1); + assert(bits[1].data[0].data == "script"_s); + if (bits[2].data.size() != 1) + { + return Err(bits[2].span.error_str("expect 1 ,component,s"_s)); + } + if (bits[3].data.size() != 3) + { + return Err(bits[3].span.error_str("expect 3 ,component,s"_s)); + } + + ScriptMap script_map; + script_map.span = span; + TRY_EXTRACT(bits[0].data[0], script_map.m); + TRY_EXTRACT(bits[0].data[1], script_map.x); + TRY_EXTRACT(bits[0].data[2], script_map.y); + TRY_EXTRACT(bits[0].data[3], script_map.d); + script_map.key_span = bits[1].data[0].span; + TRY_EXTRACT(bits[2].data[0], script_map.name); + TRY_EXTRACT(bits[3].data[0], script_map.npc_class); + TRY_EXTRACT(bits[3].data[1], script_map.xs); + TRY_EXTRACT(bits[3].data[2], script_map.ys); + // also expect '{' and parse real script + script_map.body = TRY(script::parse::parse_script_body(lr)); + return Ok(std::move(script_map)); + } + static + Result<std::unique_ptr<Script>> parse_script_any(io::LineSpan span, std::vector<Spanned<std::vector<Spanned<RString>>>>& bits, io::LineCharReader& lr) + { + std::unique_ptr<Script> rv; + // 4 cases: + // ScriptFunction: function|script|Fun Name{code} + // ScriptNone: -|script|script name|-1{code} + // ScriptMapNone: m,x,y,d|script|script name|-1{code} + // ScriptMap: m,x,y,d|script|script name|class,xs,ys{code} + if (bits[0].data[0].data == "function"_s) + { + rv = make_unique<ScriptFunction>(TRY(parse_script_function(span, bits, lr))); + } + else if (bits[0].data[0].data == "-"_s) + { + rv = make_unique<ScriptNone>(TRY(parse_script_none(span, bits, lr))); + } + else if (bits.size() >= 4 && bits[3].data[0].data == "-1"_s) + { + rv = make_unique<ScriptMapNone>(TRY(parse_script_map_none(span, bits, lr))); + } + else + { + rv = make_unique<ScriptMap>(TRY(parse_script_map(span, bits, lr))); + } + return Ok(std::move(rv)); + } + + /// Try to extract a top-level token + /// Return None at EOL, or Some(span) + /// This will alternate betweeen returning words and separators + static + Option<Spanned<RString>> lex(io::LineCharReader& lr, bool first) + { + io::LineChar c; + // at start of line, skip whitespace + if (first) + { + while (lr.get(c) && (/*c.ch() == ' ' ||*/ c.ch() == '\n')) + { + lr.adv(); + } + } + // at end of file, end of line, or start of script, signal end + if (!lr.get(c) || c.ch() == '\n' || c.ch() == '{') + { + return None; + } + // separators are simple + if (c.ch() == '|' || c.ch() == ',') + { + Spanned<RString> bit; + bit.span.begin = c; + bit.span.end = c; + bit.data = RString(VString<1>(c.ch())); + lr.adv(); + return Some(bit); + } + io::LineSpan span; + MString accum; + accum += c.ch(); + span.begin = c; + span.end = c; + lr.adv(); + if (c.ch() != '/') + first = false; + + // if one-char token followed by an end-of-line or separator, stop + if (!lr.get(c) || c.ch() == '\n' || c.ch() == '{' || c.ch() == ',' || c.ch() == '|') + { + Spanned<RString> bit; + bit.span = span; + bit.data = RString(accum); + return Some(bit); + } + + accum += c.ch(); + span.end = c; + lr.adv(); + + // if first token on line, can get comment + if (first && c.ch() == '/') + { + while (lr.get(c) && c.ch() != '\n' && c.ch() != '{') + { + accum += c.ch(); + span.end = c; + lr.adv(); + } + Spanned<RString> bit; + bit.span = span; + bit.data = RString(accum); + return Some(bit); + } + // otherwise, collect until an end of line or separator + while (lr.get(c) && c.ch() != '\n' && c.ch() != '{' && c.ch() != ',' && c.ch() != '|') + { + accum += c.ch(); + span.end = c; + lr.adv(); + } + Spanned<RString> bit; + bit.span = span; + bit.data = RString(accum); + return Some(std::move(bit)); + } + + Result<std::unique_ptr<TopLevel>> parse_top(io::LineCharReader& in) + { + std::unique_ptr<TopLevel> rv; + Spanned<std::vector<Spanned<std::vector<Spanned<RString>>>>> bits; + + { + Spanned<RString> mayc = TRY_UNWRAP(lex(in, true), + { + io::LineChar c; + if (in.get(c) && c.ch() == '{') + { + return Err(c.error_str("Unexpected script open"_s)); + } + return Ok(std::move(rv)); + }); + if (mayc.data.startswith("//"_s)) + { + Comment com; + com.comment = std::move(mayc.data); + com.span = std::move(mayc.span); + rv = make_unique<Comment>(std::move(com)); + return Ok(std::move(rv)); + } + + if (mayc.data == "|"_s || mayc.data == ","_s) + return Err(mayc.span.error_str("Unexpected separator"_s)); + bits.span = mayc.span; + bits.data.emplace_back(); + bits.data.back().span = mayc.span; + bits.data.back().data.push_back(mayc); + } + + while (true) + { + Spanned<RString> sep = TRY_UNWRAP(lex(in, false), + break); + if (sep.data == "|"_s) + { + bits.data.emplace_back(); + } + else if (sep.data != ","_s) + { + return Err(sep.span.error_str("Expected separator"_s)); + } + + Spanned<RString> word = TRY_UNWRAP(lex(in, false), + return Err(sep.span.error_str("Expected word after separator"_s))); + if (bits.data.back().data.empty()) + bits.data.back().span = word.span; + else + bits.data.back().span.end = word.span.end; + bits.span.end = word.span.end; + bits.data.back().data.push_back(std::move(word)); + } + + if (bits.data.size() < 2) + return Err(bits.span.error_str("Expected a line with |s in it"_s)); + for (auto& bit : bits.data) + { + if (bit.data.empty()) + return Err(bit.span.error_str("Empty components are not cool"_s)); + } + if (bits.data[1].data.size() != 1) + return Err(bits.data[1].span.error_str("Expected a single word in type position"_s)); + Spanned<RString>& w2 = bits.data[1].data[0]; + if (w2.data == "warp"_s) + { + rv = make_unique<Warp>(TRY(parse_warp(bits.span, bits.data))); + } + else if (w2.data == "shop"_s) + { + rv = make_unique<Shop>(TRY(parse_shop(bits.span, bits.data))); + } + else if (w2.data == "monster"_s) + { + rv = make_unique<Monster>(TRY(parse_monster(bits.span, bits.data))); + } + else if (w2.data == "mapflag"_s) + { + rv = make_unique<MapFlag>(TRY(parse_mapflag(bits.span, bits.data))); + } + else if (w2.data == "script"_s) + { + rv = TRY_MOVE(parse_script_any(bits.span, bits.data, in)); + } + else + { + return Err(w2.span.error_str("Unknown type"_s)); + } + return Ok(std::move(rv)); + } +} // namespace parse +} // namespace npc +} // namespace tmwa diff --git a/src/ast/npc.hpp b/src/ast/npc.hpp new file mode 100644 index 0000000..e39a704 --- /dev/null +++ b/src/ast/npc.hpp @@ -0,0 +1,135 @@ +#pragma once +// ast/npc.hpp - Structure of non-player characters (including mobs). +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "fwd.hpp" + +#include "../compat/result.hpp" + +#include "../mmo/ids.hpp" +#include "../mmo/strs.hpp" + +#include "../map/map.hpp" + +#include "script.hpp" + + +namespace tmwa +{ +namespace npc +{ +namespace parse +{ + using io::Spanned; + + struct TopLevel + { + io::LineSpan span; + + virtual ~TopLevel(); + }; + struct Comment : TopLevel + { + RString comment; + }; + struct Warp : TopLevel + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + io::LineSpan key_span; + Spanned<NpcName> name; + Spanned<unsigned> xs, ys; + Spanned<MapName> to_m; + Spanned<unsigned> to_x, to_y; + }; + struct ShopItem + { + Spanned<ItemName> name; + Spanned<int> value; + }; + struct Shop : TopLevel + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<DIR> d; + io::LineSpan key_span; + Spanned<NpcName> name; + Spanned<Species> npc_class; + Spanned<std::vector<Spanned<ShopItem>>> items; + }; + struct Monster : TopLevel + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<unsigned> xs, ys; + io::LineSpan key_span; + Spanned<MobName> name; + Spanned<Species> mob_class; + Spanned<unsigned> num; + Spanned<interval_t> delay1, delay2; + Spanned<NpcEvent> event; + }; + struct MapFlag : TopLevel + { + Spanned<MapName> m; + io::LineSpan key_span; + // TODO should this extract all the way? + Spanned<RString> name; + Spanned<RString> opt_extra; + }; + struct Script : TopLevel + { + io::LineSpan key_span; + // see src/script/parser.hpp + script::parse::ScriptBody body; + }; + struct ScriptFunction : Script + { + io::LineSpan key1_span; + Spanned<RString> name; + }; + struct ScriptNone : Script + { + io::LineSpan key1_span; + Spanned<NpcName> name; + io::LineSpan key4_span; + }; + struct ScriptMapNone : Script + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<DIR> d; + Spanned<NpcName> name; + io::LineSpan key4_span; + }; + struct ScriptMap : Script + { + Spanned<MapName> m; + Spanned<unsigned> x, y; + Spanned<DIR> d; + Spanned<NpcName> name; + Spanned<Species> npc_class; + Spanned<unsigned> xs, ys; + }; + // other Script subclasses elsewhere? (for item and magic scripts) + + Result<std::unique_ptr<TopLevel>> parse_top(io::LineCharReader& in); +} // namespace parse +} // namespace npc +} // namespace tmwa diff --git a/src/ast/npc_test.cpp b/src/ast/npc_test.cpp new file mode 100644 index 0000000..2697351 --- /dev/null +++ b/src/ast/npc_test.cpp @@ -0,0 +1,569 @@ +#include "npc.hpp" +// ast/npc_test.cpp - Testsuite for npc parser. +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include <gtest/gtest.h> + +#include "../tests/fdhack.hpp" + +//#include "../poison.hpp" + + +namespace tmwa +{ +namespace npc +{ +namespace parse +{ + static + io::FD string_pipe(ZString sz) + { + io::FD rfd, wfd; + if (-1 == io::FD::pipe(rfd, wfd)) + return io::FD(); + if (sz.size() != wfd.write(sz.c_str(), sz.size())) + { + rfd.close(); + wfd.close(); + return io::FD(); + } + wfd.close(); + return rfd; + } + +#define EXPECT_SPAN(span, bl,bc, el,ec) \ + ({ \ + EXPECT_EQ((span).begin.line, bl); \ + EXPECT_EQ((span).begin.column, bc); \ + EXPECT_EQ((span).end.line, el); \ + EXPECT_EQ((span).end.column, ec); \ + }) + + TEST(ast, eof) + { + QuietFd q; + LString inputs[] = + { + ""_s, + "\n"_s, + "\n\n"_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_EQ(res.get_success(), Some(std::unique_ptr<TopLevel>(nullptr))); + } + } + TEST(ast, comment) + { + QuietFd q; + LString inputs[] = + { + //23456789 + "// hello"_s, + "// hello\n "_s, + "// hello\nabc"_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,8); + auto p = dynamic_cast<Comment *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_EQ(p->comment, "// hello"_s); + } + } + } + TEST(ast, warp) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 4 + //234567890123456789012345678901234567890123456789 + "map.gat,1,2|warp|To Other Map|3,4,other.gat,5,6"_s, + "map.gat,1,2|warp|To Other Map|3,4,other.gat,5,6\n"_s, + "map.gat,1,2|warp|To Other Map|3,4,other.gat,5,6{"_s, + // no optional fields in warp + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,47); + auto p = dynamic_cast<Warp *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->key_span, 1,13, 1,16); + EXPECT_SPAN(p->name.span, 1,18, 1,29); + EXPECT_EQ(p->name.data, stringish<NpcName>("To Other Map"_s)); + EXPECT_SPAN(p->xs.span, 1,31, 1,31); + EXPECT_EQ(p->xs.data, 3); + EXPECT_SPAN(p->ys.span, 1,33, 1,33); + EXPECT_EQ(p->ys.data, 4); + EXPECT_SPAN(p->to_m.span, 1,35, 1,43); + EXPECT_EQ(p->to_m.data, stringish<MapName>("other"_s)); + EXPECT_SPAN(p->to_x.span, 1,45, 1,45); + EXPECT_EQ(p->to_x.data, 5); + EXPECT_SPAN(p->to_y.span, 1,47, 1,47); + EXPECT_EQ(p->to_y.data, 6); + } + } + } + TEST(ast, shop) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 4 5 + //2345678901234567890123456789012345678901234567890123456789 + "map.gat,1,2,3|shop|Flower Shop|4,5:6,Named:7,Spaced :8"_s, + "map.gat,1,2,3|shop|Flower Shop|4,5:6,Named:7,Spaced :8\n"_s, + "map.gat,1,2,3|shop|Flower Shop|4,5:6,Named:7,Spaced :8{"_s, + // no optional fields in shop + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,54); + auto p = dynamic_cast<Shop *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->d.span, 1,13, 1,13); + EXPECT_EQ(p->d.data, DIR::NW); + EXPECT_SPAN(p->key_span, 1,15, 1,18); + EXPECT_SPAN(p->name.span, 1,20, 1,30); + EXPECT_EQ(p->name.data, stringish<NpcName>("Flower Shop"_s)); + EXPECT_SPAN(p->npc_class.span, 1,32, 1,32); + EXPECT_EQ(p->npc_class.data, wrap<Species>(4)); + EXPECT_SPAN(p->items.span, 1,34, 1,54); + EXPECT_EQ(p->items.data.size(), 3); + EXPECT_SPAN(p->items.data[0].span, 1,34, 1,36); + EXPECT_SPAN(p->items.data[0].data.name.span, 1,34, 1,34); + EXPECT_EQ(p->items.data[0].data.name.data, stringish<ItemName>("5"_s)); + EXPECT_SPAN(p->items.data[0].data.value.span, 1,36, 1,36); + EXPECT_EQ(p->items.data[0].data.value.data, 6); + EXPECT_SPAN(p->items.data[1].span, 1,38, 1,44); + EXPECT_SPAN(p->items.data[1].data.name.span, 1,38, 1,42); + EXPECT_EQ(p->items.data[1].data.name.data, stringish<ItemName>("Named"_s)); + EXPECT_SPAN(p->items.data[1].data.value.span, 1,44, 1,44); + EXPECT_EQ(p->items.data[1].data.value.data, 7); + EXPECT_SPAN(p->items.data[2].span, 1,46, 1,54); + EXPECT_SPAN(p->items.data[2].data.name.span, 1,46, 1,52); + EXPECT_EQ(p->items.data[2].data.name.data, stringish<ItemName>("Spaced"_s)); + EXPECT_SPAN(p->items.data[2].data.value.span, 1,54, 1,54); + EXPECT_EQ(p->items.data[2].data.value.data, 8); + } + } + } + TEST(ast, monster) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 4 5 6 + //23456789012345678901234567890123456789012345678901234567890123456789 + "map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000,Npc::Event"_s, + "map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000,Npc::Event\n"_s, + "map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000,Npc::Event{"_s, + "Map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000"_s, + "Map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000\n"_s, + "Map.gat,1,2,3,4|monster|Feeping Creature|5,6,7000,8000{"_s, + "nap.gat,1,20304|monster|Feeping Creature|506,700008000"_s, + "nap.gat,1,20304|monster|Feeping Creature|506,700008000\n"_s, + "nap.gat,1,20304|monster|Feeping Creature|506,700008000{"_s, + }; + for (auto input : inputs) + { + bool first = input.startswith('m'); + bool second = input.startswith('M'); + bool third = input.startswith('n'); + assert(first + second + third == 1); + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,first?65:54); + auto p = dynamic_cast<Monster *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + if (first) + { + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + } + else if (second) + { + EXPECT_EQ(p->m.data, stringish<MapName>("Map"_s)); + } + else + { + EXPECT_EQ(p->m.data, stringish<MapName>("nap"_s)); + } + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + if (!third) + { + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->xs.span, 1,13, 1,13); + EXPECT_EQ(p->xs.data, 3); + EXPECT_SPAN(p->ys.span, 1,15, 1,15); + EXPECT_EQ(p->ys.data, 4); + } + else + { + EXPECT_SPAN(p->y.span, 1,11, 1,15); + EXPECT_EQ(p->y.data, 20304); + EXPECT_SPAN(p->xs.span, 1,16, 1,16); + EXPECT_EQ(p->xs.data, 0); + EXPECT_SPAN(p->ys.span, 1,16, 1,16); + EXPECT_EQ(p->ys.data, 0); + } + EXPECT_SPAN(p->key_span, 1,17, 1,23); + EXPECT_SPAN(p->name.span, 1,25, 1,40); + EXPECT_EQ(p->name.data, stringish<MobName>("Feeping Creature"_s)); + if (!third) + { + EXPECT_SPAN(p->mob_class.span, 1,42, 1,42); + EXPECT_EQ(p->mob_class.data, wrap<Species>(5)); + EXPECT_SPAN(p->num.span, 1,44, 1,44); + EXPECT_EQ(p->num.data, 6); + EXPECT_SPAN(p->delay1.span, 1,46, 1,49); + EXPECT_EQ(p->delay1.data, 7_s); + EXPECT_SPAN(p->delay2.span, 1,51, 1,54); + EXPECT_EQ(p->delay2.data, 8_s); + } + else + { + EXPECT_SPAN(p->mob_class.span, 1,42, 1,44); + EXPECT_EQ(p->mob_class.data, wrap<Species>(506)); + EXPECT_SPAN(p->num.span, 1,46, 1,54); + EXPECT_EQ(p->num.data, 700008000); + EXPECT_SPAN(p->delay1.span, 1,55, 1,55); + EXPECT_EQ(p->delay1.data, 0_s); + EXPECT_SPAN(p->delay2.span, 1,55, 1,55); + EXPECT_EQ(p->delay2.data, 0_s); + } + if (first) + { + EXPECT_SPAN(p->event.span, 1,56, 1,65); + EXPECT_EQ(p->event.data.npc, stringish<NpcName>("Npc"_s)); + EXPECT_EQ(p->event.data.label, stringish<ScriptLabel>("Event"_s)); + } + else + { + EXPECT_SPAN(p->event.span, 1,55, 1,55); + EXPECT_EQ(p->event.data.npc, NpcName()); + EXPECT_EQ(p->event.data.label, ScriptLabel()); + } + } + } + } + TEST(ast, mapflag) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "map.gat|mapflag|flagname"_s, + "map.gat|mapflag|flagname\n"_s, + "map.gat|mapflag|flagname{"_s, + "Map.gat|mapflag|flagname|optval"_s, + "Map.gat|mapflag|flagname|optval\n"_s, + "Map.gat|mapflag|flagname|optval{"_s, + }; + for (auto input : inputs) + { + bool second = input.startswith('M'); + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,!second?24:31); + auto p = dynamic_cast<MapFlag *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + if (!second) + { + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + } + else + { + EXPECT_EQ(p->m.data, stringish<MapName>("Map"_s)); + } + EXPECT_SPAN(p->key_span, 1,9, 1,15); + EXPECT_SPAN(p->name.span, 1,17, 1,24); + EXPECT_EQ(p->name.data, "flagname"_s); + if (!second) + { + EXPECT_SPAN(p->opt_extra.span, 1,25, 1,25); + EXPECT_EQ(p->opt_extra.data, ""_s); + } + else + { + EXPECT_SPAN(p->opt_extra.span, 1,26, 1,31); + EXPECT_EQ(p->opt_extra.data, "optval"_s); + } + } + } + } + + TEST(ast, scriptfun) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "function|script|Fun Name{end;}"_s, + // 123456 + "function|script|Fun Name\n{end;}\n"_s, + // 1234567 + "function|script|Fun Name\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,24); + auto p = dynamic_cast<ScriptFunction *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->key1_span, 1,1, 1,8); + EXPECT_SPAN(p->key_span, 1,10, 1,15); + EXPECT_SPAN(p->name.span, 1,17, 1,24); + EXPECT_EQ(p->name.data, "Fun Name"_s); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,25, 1,30); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } + TEST(ast, scriptnone) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "-|script|#config|-1{end;}"_s, + // 123456 + "-|script|#config|-1\n{end;}\n"_s, + // 1234567 + "-|script|#config|-1\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,19); + auto p = dynamic_cast<ScriptNone *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->key1_span, 1,1, 1,1); + EXPECT_SPAN(p->key_span, 1,3, 1,8); + EXPECT_SPAN(p->name.span, 1,10, 1,16); + EXPECT_EQ(p->name.data, stringish<NpcName>("#config"_s)); + EXPECT_SPAN(p->key4_span, 1,18, 1,19); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,20, 1,25); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } + TEST(ast, scriptmapnone) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "map.gat,1,2,3|script|Init|-1{end;}"_s, + "map.gat,1,2,3|script|Init|-1\n{end;}\n"_s, + "map.gat,1,2,3|script|Init|-1\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,28); + auto p = dynamic_cast<ScriptMapNone *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->d.span, 1,13, 1,13); + EXPECT_EQ(p->d.data, DIR::NW); + EXPECT_SPAN(p->key_span, 1,15, 1,20); + EXPECT_SPAN(p->name.span, 1,22, 1,25); + EXPECT_EQ(p->name.data, stringish<NpcName>("Init"_s)); + EXPECT_SPAN(p->key4_span, 1,27, 1,28); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,29, 1,34); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } + TEST(ast, scriptmap) + { + QuietFd q; + LString inputs[] = + { + // 1 2 3 + //23456789012345678901234567890123456789 + "map.gat,1,2,3|script|Asdf|4,5,6{end;}"_s, + "map.gat,1,2,3|script|Asdf|4,5,6\n{end;}\n"_s, + "map.gat,1,2,3|script|Asdf|4,5,6\n \n {end;} "_s, + }; + for (auto input : inputs) + { + io::LineCharReader lr("<string>"_s, string_pipe(input)); + auto res = parse_top(lr); + EXPECT_TRUE(res.get_success().is_some()); + auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL()); + EXPECT_SPAN(top->span, 1,1, 1,31); + auto p = dynamic_cast<ScriptMap *>(top.get()); + EXPECT_TRUE(p); + if (p) + { + EXPECT_SPAN(p->m.span, 1,1, 1,7); + EXPECT_EQ(p->m.data, stringish<MapName>("map"_s)); + EXPECT_SPAN(p->x.span, 1,9, 1,9); + EXPECT_EQ(p->x.data, 1); + EXPECT_SPAN(p->y.span, 1,11, 1,11); + EXPECT_EQ(p->y.data, 2); + EXPECT_SPAN(p->d.span, 1,13, 1,13); + EXPECT_EQ(p->d.data, DIR::NW); + EXPECT_SPAN(p->key_span, 1,15, 1,20); + EXPECT_SPAN(p->name.span, 1,22, 1,25); + EXPECT_EQ(p->name.data, stringish<NpcName>("Asdf"_s)); + EXPECT_SPAN(p->npc_class.span, 1,27, 1,27); + EXPECT_EQ(p->npc_class.data, wrap<Species>(4)); + EXPECT_SPAN(p->xs.span, 1,29, 1,29); + EXPECT_EQ(p->xs.data, 5); + EXPECT_SPAN(p->ys.span, 1,31, 1,31); + EXPECT_EQ(p->ys.data, 6); + if (input.endswith('}')) + { + EXPECT_SPAN(p->body.span, 1,32, 1,37); + } + else if (input.endswith('\n')) + { + EXPECT_SPAN(p->body.span, 2,1, 2,6); + } + else if (input.endswith(' ')) + { + EXPECT_SPAN(p->body.span, 3,2, 3,7); + } + else + { + FAIL(); + } + EXPECT_EQ(p->body.braced_body, "{end;}"_s); + } + } + } +} // namespace parse +} // namespace npc +} // namespace tmwa diff --git a/src/ast/script.cpp b/src/ast/script.cpp new file mode 100644 index 0000000..cc67224 --- /dev/null +++ b/src/ast/script.cpp @@ -0,0 +1,71 @@ +#include "script.hpp" +// ast/script.cpp - Structure of tmwa-script +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "../poison.hpp" + + +namespace tmwa +{ +namespace script +{ +namespace parse +{ + Result<ScriptBody> parse_script_body(io::LineCharReader& lr) + { + io::LineSpan span; + io::LineChar c; + while (true) + { + if (!lr.get(c)) + { + return Err("error: unexpected EOF before '{' in parse_script_body"_s); + } + if (c.ch() == ' ' || c.ch() == '\n') + { + lr.adv(); + continue; + } + break; + } + if (c.ch() != '{') + { + return Err(c.error_str("expected opening '{'"_s)); + } + + MString accum; + accum += c.ch(); + span.begin = c; + lr.adv(); + while (true) + { + if (!lr.get(c)) + return Err(c.error_str("unexpected EOF before '}' in parse_script_body"_s)); + accum += c.ch(); + span.end = c; + lr.adv(); + if (c.ch() == '}') + { + return Ok(ScriptBody{RString(accum), std::move(span)}); + } + } + } +} // namespace parse +} // namespace script +} // namespace tmwa diff --git a/src/ast/script.hpp b/src/ast/script.hpp new file mode 100644 index 0000000..59e53f0 --- /dev/null +++ b/src/ast/script.hpp @@ -0,0 +1,94 @@ +#pragma once +// ast/script.hpp - Structure of tmwa-script +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "fwd.hpp" + +#include "../compat/result.hpp" + +#include "../io/line.hpp" + + +namespace tmwa +{ +namespace script +{ +namespace parse +{ + using io::Spanned; + + struct ScriptBody + { + RString braced_body; + io::LineSpan span; + }; + + Result<ScriptBody> parse_script_body(io::LineCharReader& lr); + + /* + (-- First bare-body-chunk only allowed for npcs, items, magic, functions. + It is not allowed for events. Basically it's an implicit label at times. + Last normal-lines is only permitted on item and magic scripts. --) + { script-body }: "{" bare-body-chunk? body-chunk* normal-lines? "}" + body-chunk: (comment* labelname ":")+ bare-body-chunk + bare-body-chunk: normal-lines terminator-line + normal-lines: normal-line* + any-line: normal-line + any-line: terminator-line + normal-line: "if" "(" expr ")" any-line + normal-line: normal-command ((expr ",")* expr)? ";" + terminator-line: "menu" (expr, labelname)* expr, labelname ";" + terminator-line: "goto" labelname ";" + terminator-line: terminator ((expr ",")* expr)? ";" + terminator: "return" + terminator: "close" + terminator: "end" + terminator: "mapexit" + terminator: "shop" + + expr: subexpr_-1 + subexpr_N: ("+" | "-" | "!" | "~") subexpr_7 + subexpr_N: simple-expr (op_M subexpr_M | "(" ((expr ",")+ expr)? ")")* if N < M; function call only if N < 8 and preceding simple-expr (op sub)* is a known function + op_0: "||" + op_1: "&&" + op_2: "==" + op_2: "!=" + op_2: ">=" + op_2: ">" + op_2: "<=" + op_2: "<" + op_3: "^" + op_4: "|" + op_5: "&" + op_5: ">>" + op_5: "<<" + op_6: "+" + op_6: "-" + op_7: "*" + op_7: "/" + op_7: "%" + simple-expr: "(" expr ")" + simple-expr: integer + simple-expr: string + simple-expr: variable ("[" expr "]")? + simple-expr: function // no longer command/label though + */ +} // namespace parse +} // namespace script +} // namespace tmwa diff --git a/src/compat/result.hpp b/src/compat/result.hpp new file mode 100644 index 0000000..f03c026 --- /dev/null +++ b/src/compat/result.hpp @@ -0,0 +1,78 @@ +#pragma once +// result.hpp - A possibly failed return value +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include "fwd.hpp" + +#include "../strings/rstring.hpp" + +#include "option.hpp" + +namespace tmwa +{ + namespace result + { + enum ResultMagicFlag { magic_flag }; + + template<class T> + class Result + { + Option<T> success; + RString failure; + public: + Result(ResultMagicFlag, T v) + : success(Some(std::move(v))), failure() + {} + Result(ResultMagicFlag, RString msg) + : success(None), failure(std::move(msg)) + {} + + bool is_ok() { return success.is_some(); } + bool is_err() { return !is_ok(); } + + Option<T>& get_success() { return success; } + RString& get_failure() { return failure; } + }; + + template<class T> + Result<T> Ok(T v) + { + return Result<T>(magic_flag, std::move(v)); + } + + struct Err + { + RString message; + Err(RString m) : message(std::move(m)) {} + + template<class T> + operator Result<T>() + { + return Result<T>(magic_flag, message); + } + }; + } // namespace result + using result::Result; + using result::Ok; + using result::Err; + +#define TRY(r) ({ auto _res = r; TRY_UNWRAP(_res.get_success(), return ::tmwa::Err(_res.get_failure())); }) + // TODO the existence of this as a separate macro is a bug. +#define TRY_MOVE(r) ({ auto _res = r; TRY_UNWRAP(std::move(_res.get_success()), return ::tmwa::Err(_res.get_failure())); }) +} // namespace tmwa diff --git a/src/compat/result_test.cpp b/src/compat/result_test.cpp new file mode 100644 index 0000000..0fcc181 --- /dev/null +++ b/src/compat/result_test.cpp @@ -0,0 +1,79 @@ +#include "result.hpp" +// result_test.cpp - Testsuite for possibly failing return values +// +// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com> +// +// This file is part of The Mana World (Athena server) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +#include <gtest/gtest.h> + +#include "../strings/literal.hpp" + +//#include "../poison.hpp" + + +namespace tmwa +{ +TEST(Result, inspect) +{ + struct Foo + { + int val; + + Foo(int v) : val(v) {} + Foo(Foo&&) = default; + Foo(const Foo&) = delete; + Foo& operator = (Foo&&) = default; + Foo& operator = (const Foo&) = delete; + + bool operator == (const Foo& other) const + { + return this->val == other.val; + } + }; + + Result<Foo> foo = Ok(Foo(1)); + EXPECT_TRUE(foo.is_ok()); + EXPECT_FALSE(foo.is_err()); + EXPECT_EQ(foo.get_success(), Some(Foo(1))); + EXPECT_EQ(foo.get_failure(), ""_s); + foo = Err("oops"_s); + EXPECT_FALSE(foo.is_ok()); + EXPECT_TRUE(foo.is_err()); + EXPECT_EQ(foo.get_success(), None<Foo>()); + EXPECT_EQ(foo.get_failure(), "oops"_s); +} + +static +Result<int> try_you(bool b) +{ + return b ? Ok(0) : Err("die"_s); +} + +static +Result<int> try_me(bool b) +{ + return Ok(TRY(try_you(b)) + 1); +} + +TEST(Result, try) +{ + Result<int> t = try_me(true); + EXPECT_EQ(t.get_success(), Some(1)); + Result<int> f = try_me(false); + EXPECT_EQ(f.get_failure(), "die"_s); +} +} // namespace tmwa diff --git a/src/io/line.hpp b/src/io/line.hpp index 8244c5e..5572e98 100644 --- a/src/io/line.hpp +++ b/src/io/line.hpp @@ -41,6 +41,9 @@ namespace io uint16_t line, column; AString message_str(ZString cat, ZString msg) const; + AString note_str(ZString msg) const { return message_str("note"_s, msg); } + AString warning_str(ZString msg) const { return message_str("warning"_s, msg); } + AString error_str(ZString msg) const { return message_str("error"_s, msg); } void message(ZString cat, ZString msg) const; void note(ZString msg) const { message("note"_s, msg); } void warning(ZString msg) const { message("warning"_s, msg); } @@ -64,12 +67,22 @@ namespace io LineChar begin, end; AString message_str(ZString cat, ZString msg) const; + AString note_str(ZString msg) const { return message_str("note"_s, msg); } + AString warning_str(ZString msg) const { return message_str("warning"_s, msg); } + AString error_str(ZString msg) const { return message_str("error"_s, msg); } void message(ZString cat, ZString msg) const; void note(ZString msg) const { message("note"_s, msg); } void warning(ZString msg) const { message("warning"_s, msg); } void error(ZString msg) const { message("error"_s, msg); } }; + template<class T> + struct Spanned + { + T data; + LineSpan span; + }; + class LineReader { protected: diff --git a/src/io/line_test.cpp b/src/io/line_test.cpp index edf60bd..46e7b57 100644 --- a/src/io/line_test.cpp +++ b/src/io/line_test.cpp @@ -23,6 +23,8 @@ #include "../strings/astring.hpp" #include "../strings/zstring.hpp" +#include "../tests/fdhack.hpp" + #include "../poison.hpp" @@ -57,6 +59,7 @@ TEST(io, line1) } TEST(io, line2) { + QuietFd q; io::LineReader lr("<string2>"_s, string_pipe("Hello\nWorld"_s)); io::Line hi; EXPECT_TRUE(lr.read_line(hi)); @@ -73,6 +76,7 @@ TEST(io, line2) } TEST(io, line3) { + QuietFd q; io::LineReader lr("<string3>"_s, string_pipe("Hello\rWorld"_s)); io::Line hi; EXPECT_TRUE(lr.read_line(hi)); @@ -89,6 +93,7 @@ TEST(io, line3) } TEST(io, line4) { + QuietFd q; io::LineReader lr("<string4>"_s, string_pipe("Hello\r\nWorld"_s)); io::Line hi; EXPECT_TRUE(lr.read_line(hi)); @@ -105,6 +110,7 @@ TEST(io, line4) } TEST(io, line5) { + QuietFd q; io::LineReader lr("<string5>"_s, string_pipe("Hello\n\rWorld"_s)); io::Line hi; EXPECT_TRUE(lr.read_line(hi)); @@ -175,6 +181,7 @@ TEST(io, linechar1) } TEST(io, linechar2) { + QuietFd q; io::LineCharReader lr("<stringchar2>"_s, string_pipe("Hi\nWu"_s)); io::LineChar c; EXPECT_TRUE(lr.get(c)); @@ -223,6 +230,7 @@ TEST(io, linechar2) } TEST(io, linechar3) { + QuietFd q; io::LineCharReader lr("<stringchar3>"_s, string_pipe("Hi\rWu"_s)); io::LineChar c; EXPECT_TRUE(lr.get(c)); @@ -271,6 +279,7 @@ TEST(io, linechar3) } TEST(io, linechar4) { + QuietFd q; io::LineCharReader lr("<stringchar4>"_s, string_pipe("Hi\r\nWu"_s)); io::LineChar c; EXPECT_TRUE(lr.get(c)); @@ -319,6 +328,7 @@ TEST(io, linechar4) } TEST(io, linechar5) { + QuietFd q; io::LineCharReader lr("<stringchar5>"_s, string_pipe("Hi\n\rWu"_s)); io::LineChar c; EXPECT_TRUE(lr.get(c)); @@ -402,17 +412,17 @@ TEST(io, linespan) } while (span.end.ch() != 'r'); - EXPECT_EQ(span.begin.message_str("note"_s, "foo"_s), + EXPECT_EQ(span.begin.note_str("foo"_s), "<span>:1:5: note: foo\n" "Hello,\n" " ^\n"_s ); - EXPECT_EQ(span.end.message_str("warning"_s, "bar"_s), + EXPECT_EQ(span.end.warning_str("bar"_s), "<span>:2:3: warning: bar\n" "World!\n" " ^\n"_s ); - EXPECT_EQ(span.message_str("error"_s, "qux"_s), + EXPECT_EQ(span.error_str("qux"_s), "<span>:1:5: error: qux\n" "Hello,\n" " ^~ ...\n" diff --git a/src/io/read.cpp b/src/io/read.cpp index 3ae5246..f3ed293 100644 --- a/src/io/read.cpp +++ b/src/io/read.cpp @@ -36,6 +36,8 @@ namespace io { ReadFile::ReadFile(FD f) : fd(f), start(0), end(0) + // only for debug-sanity + , buf{} { } ReadFile::ReadFile(ZString name) diff --git a/src/io/read_test.cpp b/src/io/read_test.cpp index 8fe84b7..e655eb1 100644 --- a/src/io/read_test.cpp +++ b/src/io/read_test.cpp @@ -24,6 +24,8 @@ #include "../strings/zstring.hpp" #include "../strings/literal.hpp" +#include "../tests/fdhack.hpp" + #include "../poison.hpp" @@ -47,6 +49,7 @@ io::FD string_pipe(ZString sz) TEST(io, read1) { + QuietFd q; io::ReadFile rf(string_pipe("Hello"_s)); AString hi; EXPECT_TRUE(rf.getline(hi)); @@ -63,6 +66,7 @@ TEST(io, read2) } TEST(io, read3) { + QuietFd q; io::ReadFile rf(string_pipe("Hello\r"_s)); AString hi; EXPECT_TRUE(rf.getline(hi)); @@ -71,6 +75,7 @@ TEST(io, read3) } TEST(io, read4) { + QuietFd q; io::ReadFile rf(string_pipe("Hello\r\n"_s)); AString hi; EXPECT_TRUE(rf.getline(hi)); @@ -79,6 +84,7 @@ TEST(io, read4) } TEST(io, read5) { + QuietFd q; io::ReadFile rf(string_pipe("Hello\n\r"_s)); AString hi; EXPECT_TRUE(rf.getline(hi)); diff --git a/src/io/write.cpp b/src/io/write.cpp index 5359c7a..a98954b 100644 --- a/src/io/write.cpp +++ b/src/io/write.cpp @@ -37,6 +37,8 @@ namespace io { WriteFile::WriteFile(FD f, bool linebuffered) : fd(f), lb(linebuffered), buflen(0) + // only for debug-sanity + , buf{} {} WriteFile::WriteFile(ZString name, bool linebuffered) : fd(FD::open(name, O_WRONLY | O_CREAT | O_TRUNC, 0666)), lb(linebuffered), buflen(0) diff --git a/src/map/clif.cpp b/src/map/clif.cpp index b0cb541..a5b7278 100644 --- a/src/map/clif.cpp +++ b/src/map/clif.cpp @@ -5675,4 +5675,38 @@ void do_init_clif(void) { make_listen_port(map_port, SessionParsers{.func_parse= clif_parse, .func_delete= clif_delete}); } + +bool extract(XString str, DIR *d) +{ + unsigned di; + if (extract(str, &di) && di < 8) + { + *d = static_cast<DIR>(di); + return true; + } + const struct + { + LString str; + DIR d; + } dirs[] = + { + {"S"_s, DIR::S}, + {"SW"_s, DIR::SW}, + {"W"_s, DIR::W}, + {"NW"_s, DIR::NW}, + {"N"_s, DIR::N}, + {"NE"_s, DIR::NE}, + {"E"_s, DIR::E}, + {"SE"_s, DIR::SE}, + }; + for (auto& pair : dirs) + { + if (str == pair.str) + { + *d = pair.d; + return true; + } + } + return false; +} } // namespace tmwa diff --git a/src/map/clif.hpp b/src/map/clif.hpp index 12ed633..9873592 100644 --- a/src/map/clif.hpp +++ b/src/map/clif.hpp @@ -189,4 +189,6 @@ int clif_GM_kick(dumb_ptr<map_session_data> sd, dumb_ptr<map_session_data> tsd, int clif_foreachclient(std::function<void(dumb_ptr<map_session_data>)>); void do_init_clif(void); + +bool extract(XString, DIR *); } // namespace tmwa diff --git a/src/mmo/extract.cpp b/src/mmo/extract.cpp index a480984..9123237 100644 --- a/src/mmo/extract.cpp +++ b/src/mmo/extract.cpp @@ -30,6 +30,9 @@ #include "../poison.hpp" +// TODO move this whole file to io/ or something. +// It needs to be lower in the include hierarchy so it can be implemented +// for library types. Also it should pass an io::LineSpan around. namespace tmwa { bool extract(XString str, XString *rv) @@ -38,6 +41,12 @@ bool extract(XString str, XString *rv) return true; } +bool extract(XString str, RString *rv) +{ + *rv = str; + return true; +} + bool extract(XString str, AString *rv) { *rv = str; @@ -98,4 +107,174 @@ bool extract(XString str, CharName *out) } return false; } + +bool extract(XString str, std::chrono::nanoseconds *ns) +{ + std::chrono::nanoseconds::rep rep; + if (extract(str, &rep)) + { + *ns = std::chrono::nanoseconds(rep); + return true; + } + if (str.endswith("ns"_s)) + { + if (extract(str.xrslice_h("ns"_s.size()), &rep)) + { + *ns = std::chrono::nanoseconds(rep); + return true; + } + return false; + } + std::chrono::microseconds bigger; + if (extract(str, &bigger)) + { + *ns = bigger; + return *ns == bigger; + } + return false; +} +bool extract(XString str, std::chrono::microseconds *us) +{ + std::chrono::microseconds::rep rep; + if (extract(str, &rep)) + { + *us = std::chrono::microseconds(rep); + return true; + } + if (str.endswith("us"_s)) + { + if (extract(str.xrslice_h("us"_s.size()), &rep)) + { + *us = std::chrono::microseconds(rep); + return true; + } + return false; + } + std::chrono::milliseconds bigger; + if (extract(str, &bigger)) + { + *us = bigger; + return *us == bigger; + } + return false; +} +bool extract(XString str, std::chrono::milliseconds *ms) +{ + std::chrono::milliseconds::rep rep; + if (extract(str, &rep)) + { + *ms = std::chrono::milliseconds(rep); + return true; + } + if (str.endswith("ms"_s)) + { + if (extract(str.xrslice_h("ms"_s.size()), &rep)) + { + *ms = std::chrono::milliseconds(rep); + return true; + } + return false; + } + std::chrono::seconds bigger; + if (extract(str, &bigger)) + { + *ms = bigger; + return *ms == bigger; + } + return false; +} +bool extract(XString str, std::chrono::seconds *s) +{ + std::chrono::seconds::rep rep; + if (extract(str, &rep)) + { + *s = std::chrono::seconds(rep); + return true; + } + if (str.endswith("s"_s)) + { + if (extract(str.xrslice_h("s"_s.size()), &rep)) + { + *s = std::chrono::seconds(rep); + return true; + } + return false; + } + std::chrono::minutes bigger; + if (extract(str, &bigger)) + { + *s = bigger; + return *s == bigger; + } + return false; +} +bool extract(XString str, std::chrono::minutes *min) +{ + std::chrono::minutes::rep rep; + if (extract(str, &rep)) + { + *min = std::chrono::minutes(rep); + return true; + } + if (str.endswith("min"_s)) + { + if (extract(str.xrslice_h("min"_s.size()), &rep)) + { + *min = std::chrono::minutes(rep); + return true; + } + return false; + } + std::chrono::hours bigger; + if (extract(str, &bigger)) + { + *min = bigger; + return *min == bigger; + } + return false; +} +bool extract(XString str, std::chrono::hours *h) +{ + std::chrono::hours::rep rep; + if (extract(str, &rep)) + { + *h = std::chrono::hours(rep); + return true; + } + if (str.endswith("h"_s)) + { + if (extract(str.xrslice_h("h"_s.size()), &rep)) + { + *h = std::chrono::hours(rep); + return true; + } + return false; + } + std::chrono::duration<int, std::ratio<60*60*24>> bigger; + if (extract(str, &bigger)) + { + *h = bigger; + return *h == bigger; + } + return false; +} +bool extract(XString str, std::chrono::duration<int, std::ratio<60*60*24>> *d) +{ + std::chrono::duration<int, std::ratio<60*60*24>>::rep rep; + if (extract(str, &rep)) + { + *d = std::chrono::duration<int, std::ratio<60*60*24>>(rep); + return true; + } + if (str.endswith("d"_s)) + { + if (extract(str.xrslice_h("d"_s.size()), &rep)) + { + *d = std::chrono::duration<int, std::ratio<60*60*24>>(rep); + return true; + } + return false; + } + return false; +} } // namespace tmwa diff --git a/src/mmo/extract.hpp b/src/mmo/extract.hpp index ed2eb78..152d1b7 100644 --- a/src/mmo/extract.hpp +++ b/src/mmo/extract.hpp @@ -24,6 +24,7 @@ #include <cstdlib> #include <algorithm> +#include <chrono> #include <vector> #include "../ints/wrap.hpp" @@ -93,7 +94,7 @@ bool extract_as_int(XString str, T *iv) } bool extract(XString str, XString *rv); - +bool extract(XString str, RString *rv); bool extract(XString str, AString *rv); template<uint8_t N> @@ -228,4 +229,12 @@ bool extract(XString str, Wrapped<R> *w) { return extract(str, &w->_value); } + +bool extract(XString str, std::chrono::nanoseconds *ns); +bool extract(XString str, std::chrono::microseconds *us); +bool extract(XString str, std::chrono::milliseconds *ms); +bool extract(XString str, std::chrono::seconds *s); +bool extract(XString str, std::chrono::minutes *min); +bool extract(XString str, std::chrono::hours *h); +bool extract(XString str, std::chrono::duration<int, std::ratio<60*60*24>> *d); } // namespace tmwa diff --git a/src/mmo/extract_test.cpp b/src/mmo/extract_test.cpp index e6dc7b2..9c203ac 100644 --- a/src/mmo/extract_test.cpp +++ b/src/mmo/extract_test.cpp @@ -357,4 +357,92 @@ TEST(extract, mapname) EXPECT_TRUE(extract("abcdefghijklmno.gat"_s, &map)); EXPECT_EQ(map, "abcdefghijklmno"_s); } + +TEST(extract, chrono) +{ + std::chrono::nanoseconds ns; + std::chrono::microseconds us; + std::chrono::milliseconds ms; + std::chrono::seconds s; + std::chrono::minutes min; + std::chrono::hours h; + std::chrono::duration<int, std::ratio<60*60*24>> d; + + EXPECT_TRUE(extract("1"_s, &ns)); + EXPECT_EQ(ns, 1_ns); + EXPECT_TRUE(extract("3ns"_s, &ns)); + EXPECT_EQ(ns, 3_ns); + EXPECT_TRUE(extract("4us"_s, &ns)); + EXPECT_EQ(ns, 4_us); + EXPECT_TRUE(extract("5ms"_s, &ns)); + EXPECT_EQ(ns, 5_ms); + EXPECT_TRUE(extract("6s"_s, &ns)); + EXPECT_EQ(ns, 6_s); + EXPECT_TRUE(extract("7min"_s, &ns)); + EXPECT_EQ(ns, 7_min); + EXPECT_TRUE(extract("8h"_s, &ns)); + EXPECT_EQ(ns, 8_h); + EXPECT_TRUE(extract("9d"_s, &ns)); + EXPECT_EQ(ns, 9_d); + + EXPECT_TRUE(extract("1"_s, &us)); + EXPECT_EQ(us, 1_us); + EXPECT_TRUE(extract("4us"_s, &us)); + EXPECT_EQ(us, 4_us); + EXPECT_TRUE(extract("5ms"_s, &us)); + EXPECT_EQ(us, 5_ms); + EXPECT_TRUE(extract("6s"_s, &us)); + EXPECT_EQ(us, 6_s); + EXPECT_TRUE(extract("7min"_s, &us)); + EXPECT_EQ(us, 7_min); + EXPECT_TRUE(extract("8h"_s, &us)); + EXPECT_EQ(us, 8_h); + EXPECT_TRUE(extract("9d"_s, &us)); + EXPECT_EQ(us, 9_d); + + EXPECT_TRUE(extract("1"_s, &ms)); + EXPECT_EQ(ms, 1_ms); + EXPECT_TRUE(extract("5ms"_s, &ms)); + EXPECT_EQ(ms, 5_ms); + EXPECT_TRUE(extract("6s"_s, &ms)); + EXPECT_EQ(ms, 6_s); + EXPECT_TRUE(extract("7min"_s, &ms)); + EXPECT_EQ(ms, 7_min); + EXPECT_TRUE(extract("8h"_s, &ms)); + EXPECT_EQ(ms, 8_h); + EXPECT_TRUE(extract("9d"_s, &ms)); + EXPECT_EQ(ms, 9_d); + + EXPECT_TRUE(extract("1"_s, &s)); + EXPECT_EQ(s, 1_s); + EXPECT_TRUE(extract("6s"_s, &s)); + EXPECT_EQ(s, 6_s); + EXPECT_TRUE(extract("7min"_s, &s)); + EXPECT_EQ(s, 7_min); + EXPECT_TRUE(extract("8h"_s, &s)); + EXPECT_EQ(s, 8_h); + EXPECT_TRUE(extract("9d"_s, &s)); + EXPECT_EQ(s, 9_d); + + EXPECT_TRUE(extract("1"_s, &min)); + EXPECT_EQ(min, 1_min); + EXPECT_TRUE(extract("7min"_s, &min)); + EXPECT_EQ(min, 7_min); + EXPECT_TRUE(extract("8h"_s, &min)); + EXPECT_EQ(min, 8_h); + EXPECT_TRUE(extract("9d"_s, &min)); + EXPECT_EQ(min, 9_d); + + EXPECT_TRUE(extract("1"_s, &h)); + EXPECT_EQ(h, 1_h); + EXPECT_TRUE(extract("8h"_s, &h)); + EXPECT_EQ(h, 8_h); + EXPECT_TRUE(extract("9d"_s, &h)); + EXPECT_EQ(h, 9_d); + + EXPECT_TRUE(extract("1"_s, &d)); + EXPECT_EQ(d, 1_d); + EXPECT_TRUE(extract("9d"_s, &d)); + EXPECT_EQ(d, 9_d); +} } // namespace tmwa diff --git a/src/sexpr/lexer_test.cpp b/src/sexpr/lexer_test.cpp index fdb47f2..5904894 100644 --- a/src/sexpr/lexer_test.cpp +++ b/src/sexpr/lexer_test.cpp @@ -76,21 +76,21 @@ TEST(sexpr, lexer) sexpr::Lexer lexer("<lexer-test1>"_s, string_pipe(" foo( ) 123\"\" \n"_s)); EXPECT_EQ(lexer.peek(), sexpr::TOK_TOKEN); EXPECT_EQ(lexer.val_string(), "foo"_s); - EXPECT_EQ(lexer.span().message_str("error"_s, "test"_s), + EXPECT_EQ(lexer.span().error_str("test"_s), "<lexer-test1>:1:2: error: test\n" " foo( ) 123\"\" \n" " ^~~\n"_s ); lexer.adv(); EXPECT_EQ(lexer.peek(), sexpr::TOK_OPEN); - EXPECT_EQ(lexer.span().message_str("error"_s, "test"_s), + EXPECT_EQ(lexer.span().error_str("test"_s), "<lexer-test1>:1:5: error: test\n" " foo( ) 123\"\" \n" " ^\n"_s ); lexer.adv(); EXPECT_EQ(lexer.peek(), sexpr::TOK_CLOSE); - EXPECT_EQ(lexer.span().message_str("error"_s, "test"_s), + EXPECT_EQ(lexer.span().error_str("test"_s), "<lexer-test1>:1:7: error: test\n" " foo( ) 123\"\" \n" " ^\n"_s @@ -98,7 +98,7 @@ TEST(sexpr, lexer) lexer.adv(); EXPECT_EQ(lexer.peek(), sexpr::TOK_TOKEN); EXPECT_EQ(lexer.val_string(), "123"_s); - EXPECT_EQ(lexer.span().message_str("error"_s, "test"_s), + EXPECT_EQ(lexer.span().error_str("test"_s), "<lexer-test1>:1:9: error: test\n" " foo( ) 123\"\" \n" " ^~~\n"_s @@ -106,7 +106,7 @@ TEST(sexpr, lexer) lexer.adv(); EXPECT_EQ(lexer.peek(), sexpr::TOK_STRING); EXPECT_EQ(lexer.val_string(), ""_s); - EXPECT_EQ(lexer.span().message_str("error"_s, "test"_s), + EXPECT_EQ(lexer.span().error_str("test"_s), "<lexer-test1>:1:12: error: test\n" " foo( ) 123\"\" \n" " ^~\n"_s diff --git a/src/sexpr/parser_test.cpp b/src/sexpr/parser_test.cpp index 846d425..8619c15 100644 --- a/src/sexpr/parser_test.cpp +++ b/src/sexpr/parser_test.cpp @@ -20,6 +20,8 @@ #include <gtest/gtest.h> +#include "../tests/fdhack.hpp" + #include "../poison.hpp" @@ -90,6 +92,7 @@ TEST(sexpr, parselist) TEST(sexpr, parsebad) { + QuietFd q; for (LString bad : { "(\n"_s, ")\n"_s, diff --git a/src/strings/rstring.cpp b/src/strings/rstring.cpp index e74d1d5..5675935 100644 --- a/src/strings/rstring.cpp +++ b/src/strings/rstring.cpp @@ -58,13 +58,18 @@ namespace strings } RString& RString::operator = (const RString& r) { - // order important for self-assign - if (!r.maybe_end) - r.u.owned->count++; - if (!maybe_end && !u.owned->count--) - ::operator delete(u.owned); - u = r.u; - maybe_end = r.maybe_end; + // this turns out to be a win + // certain callers end up needing to do self-assignment a *lot*, + // leading to pointless ++,--s + if (this->u.owned != r.u.owned) + { + if (!r.maybe_end) + r.u.owned->count++; + if (!maybe_end && !u.owned->count--) + ::operator delete(u.owned); + u = r.u; + maybe_end = r.maybe_end; + } return *this; } RString& RString::operator = (RString&& r) diff --git a/src/strings/strings2_test.cpp b/src/strings/strings2_test.cpp index 8ac8482..8b91306 100644 --- a/src/strings/strings2_test.cpp +++ b/src/strings/strings2_test.cpp @@ -228,4 +228,13 @@ TEST(StringTests, rlong) EXPECT_EQ(&*r.begin(), &*r3.begin()); EXPECT_EQ(&*a.begin(), &*a3.begin()); } + +TEST(StringTest, rself) +{ + // force dynamic allocation; valgrind will check for memory errors + RString r = XString("foo bar baz"_s); + RString r2 = r; + r = r; + r = r2; +} } // namespace tmwa |