diff options
Diffstat (limited to 'src/sexpr')
-rw-r--r-- | src/sexpr/lexer.cpp | 228 | ||||
-rw-r--r-- | src/sexpr/lexer.hpp | 73 | ||||
-rw-r--r-- | src/sexpr/lexer_test.cpp | 113 | ||||
-rw-r--r-- | src/sexpr/main.cpp | 130 | ||||
-rw-r--r-- | src/sexpr/parser.cpp | 79 | ||||
-rw-r--r-- | src/sexpr/parser.hpp | 80 | ||||
-rw-r--r-- | src/sexpr/parser.py | 25 | ||||
-rw-r--r-- | src/sexpr/parser_test.cpp | 89 |
8 files changed, 817 insertions, 0 deletions
diff --git a/src/sexpr/lexer.cpp b/src/sexpr/lexer.cpp new file mode 100644 index 0000000..8c1c380 --- /dev/null +++ b/src/sexpr/lexer.cpp @@ -0,0 +1,228 @@ +#include "lexer.hpp" +// lexer.cpp - tokenize a stream of S-expressions +// +// 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 "../strings/mstring.hpp" + +#include "../io/cxxstdio.hpp" + +#include "../poison.hpp" + +namespace sexpr +{ + Lexeme Lexer::_adv() + { + XString whitespace = " \t\n\r\v\f"; + while (true) + { + if (!_in.get(_span.begin)) + { + if (!_depth.empty()) + { + _depth.back().error("Unmatched '('"); + return TOK_ERROR; + } + return TOK_EOF; + } + char co = _span.begin.ch(); + if (!whitespace.contains(co)) + break; + _in.adv(); + } + + char co = _span.begin.ch(); + _in.adv(); + _span.end = _span.begin; + switch (co) + { + case '(': + _string = "("; + _depth.push_back(_span.end); + return TOK_OPEN; + case ')': + _string = ")"; + if (_depth.empty()) + { + _span.end.error("Unmatched ')'"); + return TOK_ERROR; + } + _depth.pop_back(); + return TOK_CLOSE; + case '"': + { + MString collect; + // read until " and consume it + // but handle \s + while (true) + { + if (!_in.get(_span.end)) + { + _span.error("EOF in string literal"); + return TOK_ERROR; + } + char ch = _span.end.ch(); + _in.adv(); + if (ch == '"') + break; + + if (ch != '\\') + { + collect += ch; + continue; + } + + if (!_in.get(_span.end)) + { + _span.end.error("EOF at backslash in string"); + return TOK_ERROR; + } + ch = _span.end.ch(); + _in.adv(); + switch (ch) + { + default: + _span.end.error("Unknown backslash sequence"); + return TOK_ERROR; + case 'a': collect += '\a'; break; + case 'b': collect += '\b'; break; + case 'e': collect += '\e'; break; + case 'f': collect += '\f'; break; + case 'n': collect += '\n'; break; + case 'r': collect += '\r'; break; + case 't': collect += '\t'; break; + case 'v': collect += '\v'; break; + case '\\': collect += '\\'; break; + case '\"': collect += '\"'; break; + case 'x': + { + unsigned char tmp = 0; + for (int i = 0; i < 2; ++i) + { + tmp *= 16; + if (!_in.get(_span.end)) + { + _span.end.error("EOF after \\x in string"); + return TOK_ERROR; + } + char cx = _span.end.ch(); + _in.adv(); + if ('0' <= cx && cx <= '9') + tmp += cx - '0'; + else if ('A' <= cx && cx <= 'F') + tmp += cx - 'A' + 10; + else if ('a' <= cx && cx <= 'a') + tmp += cx - 'a' + 10; + else + { + _span.end.error("Non-hex char after \\x"); + return TOK_ERROR; + } + } + collect += tmp; + } + } + } + _string = AString(collect); + return TOK_STRING; + } + case '\'': + case '\\': + _span.end.error("forbidden character"); + return TOK_ERROR; + default: + // this includes integers - they are differentiated in parsing + { + MString collect; + collect += co; + // read until whitespace, (, ), ", or EOF + io::LineChar tmp; + while (_in.get(tmp)) + { + char ct = tmp.ch(); + if (ct == '\'' || ct == '\\') + // error later + break; + if (ct == '(' || ct == ')' || ct == '"') + break; + if (whitespace.contains(ct)) + break; + collect += ct; + _span.end = tmp; + _in.adv(); + } + _string = AString(collect); + if (!_string.is_print()) + _span.error("String is not entirely printable"); + return TOK_TOKEN; + } + } + } + + VString<4> escape(char c) + { + switch (c) + { + case '\a': return {"\\a"}; + case '\b': return {"\\b"}; + case '\e': return {"\\e"}; + case '\f': return {"\\f"}; + //case '\n': return {"\\n"}; + case '\r': return {"\\r"}; + case '\t': return {"\\t"}; + case '\v': return {"\\v"}; + case '\\': return {"\\\\"}; + case '\"': return {"\\\""}; + default: + if (c == '\n') + return c; + if (' ' <= c && c <= '~') + return c; + else + return STRNPRINTF(5, "\\x%02x", static_cast<uint8_t>(c)); + } + } + AString escape(XString s) + { + MString m; + m += '"'; + for (char c : s) + m += escape(c); + m += '"'; + return AString(m); + } + + ZString token_name(Lexeme tok) + { + switch (tok) + { + case TOK_EOF: + return ZString("EOF"); + case TOK_OPEN: + return ZString("OPEN"); + case TOK_CLOSE: + return ZString("CLOSE"); + case TOK_STRING: + return ZString("STRING"); + case TOK_TOKEN: + return ZString("TOKEN"); + default: + return ZString("ERROR"); + } + } +} // namespace sexpr diff --git a/src/sexpr/lexer.hpp b/src/sexpr/lexer.hpp new file mode 100644 index 0000000..7bce620 --- /dev/null +++ b/src/sexpr/lexer.hpp @@ -0,0 +1,73 @@ +#ifndef TMWA_SEXPR_LEXER_HPP +#define TMWA_SEXPR_LEXER_HPP +// lexer.hpp - tokenize a stream of S-expressions +// +// 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 "../sanity.hpp" + +# include "../strings/astring.hpp" +# include "../strings/vstring.hpp" +# include "../strings/xstring.hpp" +# include "../strings/zstring.hpp" + +# include "../io/line.hpp" + +namespace sexpr +{ + enum Lexeme + { + TOK_EOF = 0, + TOK_ERROR, + TOK_OPEN, + TOK_CLOSE, + TOK_STRING, + TOK_TOKEN, + }; + + class Lexer + { + io::LineCharReader _in; + Lexeme _current; + AString _string; + io::LineSpan _span; + std::vector<io::LineChar> _depth; + private: + Lexeme _adv(); + public: + explicit + Lexer(ZString filename) + : _in(filename), _current(TOK_EOF), _span(), _depth() + { adv(); } + // for unit tests + Lexer(ZString fake, io::FD fd) + : _in(fake, fd), _current(TOK_EOF), _span(), _depth() + { adv(); } + Lexeme peek() { return _current; } + void adv() { _current = _adv(); } + ZString val_string() { return _string; } + io::LineSpan span() { return _span; } + }; + + VString<4> escape(char c); + AString escape(XString s); + + ZString token_name(Lexeme tok); +} // namespace sexpr + +#endif // TMWA_SEXPR_LEXER_HPP diff --git a/src/sexpr/lexer_test.cpp b/src/sexpr/lexer_test.cpp new file mode 100644 index 0000000..7a2dc09 --- /dev/null +++ b/src/sexpr/lexer_test.cpp @@ -0,0 +1,113 @@ +#include "lexer.hpp" + +#include <gtest/gtest.h> + +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; +} + +TEST(sexpr, escape) +{ + EXPECT_EQ(sexpr::escape('\0'), "\\x00"); + EXPECT_EQ(sexpr::escape('\x1f'), "\\x1f"); + EXPECT_EQ(sexpr::escape('\x20'), " "); + EXPECT_EQ(sexpr::escape('\x7e'), "~"); + EXPECT_EQ(sexpr::escape('\x7f'), "\\x7f"); + EXPECT_EQ(sexpr::escape('\x80'), "\\x80"); + EXPECT_EQ(sexpr::escape('\xff'), "\\xff"); + EXPECT_EQ(sexpr::escape('\a'), "\\a"); + EXPECT_EQ(sexpr::escape('\b'), "\\b"); + EXPECT_EQ(sexpr::escape('\e'), "\\e"); + EXPECT_EQ(sexpr::escape('\f'), "\\f"); + //EXPECT_EQ(sexpr::escape('\n'), "\\n"); + EXPECT_EQ(sexpr::escape('\n'), "\n"); + EXPECT_EQ(sexpr::escape('\r'), "\\r"); + EXPECT_EQ(sexpr::escape('\t'), "\\t"); + EXPECT_EQ(sexpr::escape('\v'), "\\v"); + EXPECT_EQ(sexpr::escape('\\'), "\\\\"); + EXPECT_EQ(sexpr::escape('\"'), "\\\""); + + EXPECT_EQ(sexpr::escape("\x1f\x20\x7e\x7f\x80\xff\a\b\e\f\r\t\v\\\""), + "\"\\x1f ~\\x7f\\x80\\xff\\a\\b\\e\\f\\r\\t\\v\\\\\\\"\""); +} + +TEST(sexpr, lexer) +{ + io::LineSpan span; + sexpr::Lexer lexer("<lexer-test1>", string_pipe(" foo( ) 123\"\" \n")); + EXPECT_EQ(lexer.peek(), sexpr::TOK_TOKEN); + EXPECT_EQ(lexer.val_string(), "foo"); + EXPECT_EQ(lexer.span().message_str("error", "test"), + "<lexer-test1>:1:2: error: test\n" + " foo( ) 123\"\" \n" + " ^~~\n" + ); + lexer.adv(); + EXPECT_EQ(lexer.peek(), sexpr::TOK_OPEN); + EXPECT_EQ(lexer.span().message_str("error", "test"), + "<lexer-test1>:1:5: error: test\n" + " foo( ) 123\"\" \n" + " ^\n" + ); + lexer.adv(); + EXPECT_EQ(lexer.peek(), sexpr::TOK_CLOSE); + EXPECT_EQ(lexer.span().message_str("error", "test"), + "<lexer-test1>:1:7: error: test\n" + " foo( ) 123\"\" \n" + " ^\n" + ); + lexer.adv(); + EXPECT_EQ(lexer.peek(), sexpr::TOK_TOKEN); + EXPECT_EQ(lexer.val_string(), "123"); + EXPECT_EQ(lexer.span().message_str("error", "test"), + "<lexer-test1>:1:9: error: test\n" + " foo( ) 123\"\" \n" + " ^~~\n" + ); + lexer.adv(); + EXPECT_EQ(lexer.peek(), sexpr::TOK_STRING); + EXPECT_EQ(lexer.val_string(), ""); + EXPECT_EQ(lexer.span().message_str("error", "test"), + "<lexer-test1>:1:12: error: test\n" + " foo( ) 123\"\" \n" + " ^~\n" + ); + lexer.adv(); + EXPECT_EQ(lexer.peek(), sexpr::TOK_EOF); +} + +TEST(sexpr, lexbad) +{ + { + io::LineSpan span; + sexpr::Lexer lexer("<lexer-bad>", string_pipe("(\n")); + EXPECT_EQ(lexer.peek(), sexpr::TOK_OPEN); + lexer.adv(); + EXPECT_EQ(lexer.peek(), sexpr::TOK_ERROR); + } + for (ZString bad : { + ZString(")\n"), + ZString("\"\n"), + ZString("'\n"), + ZString("\\\n"), + ZString("\"\\"), + ZString("\"\\z\""), + }) + { + io::LineSpan span; + sexpr::Lexer lexer("<lexer-bad>", string_pipe(bad)); + EXPECT_EQ(lexer.peek(), sexpr::TOK_ERROR); + } +} diff --git a/src/sexpr/main.cpp b/src/sexpr/main.cpp new file mode 100644 index 0000000..7d63ddf --- /dev/null +++ b/src/sexpr/main.cpp @@ -0,0 +1,130 @@ +#include <stack> +#include <map> + +#include "../io/cxxstdio.hpp" + +#include "lexer.hpp" +#include "parser.hpp" + +#include "../poison.hpp" + +enum Spacing +{ + LINES, + SIMPLE, + SPACES, + SPACES_1, + SPACES_2, + SPACES_3, + SPACES_4, +}; + +static +void do_spacing(bool& first, Spacing& sp, int depth) +{ + if (first) + { + first = false; + return; + } + switch (sp) + { + case LINES: + PRINTF("\n%*s", (depth - 1) * 4, ""); + return; + case SPACES: + case SIMPLE: + PRINTF(" "); + return; + case SPACES_1: + PRINTF(" "); + sp = LINES; + return; + case SPACES_2: + PRINTF(" "); + sp = SPACES_1; + return; + case SPACES_3: + PRINTF(" "); + sp = SPACES_2; + return; + case SPACES_4: + PRINTF(" "); + sp = SPACES_3; + return; + } +} + +static +void adjust_spacing(Spacing& sp, ZString val) +{ + std::map<ZString, Spacing> spaces = + { + {"BLOCK", LINES}, + {"GUARD", LINES}, + {"DISABLED", LINES}, + {"PROCEDURE", SPACES_2}, + {"SPELL", SPACES_4}, + {"IF", SPACES_1}, + {"set_script_variable", SPACES_2}, + }; + auto it = spaces.find(val); + if (it != spaces.end()) + sp = it->second; +} + +int main() +{ + if (1 == 1) + { + sexpr::Lexer lexer("/dev/stdin"); + sexpr::SExpr sexpr; + while (sexpr::parse(lexer, sexpr)) + { + PRINTF(""); + } + if (lexer.peek() != sexpr::TOK_EOF) + { + lexer.span().error(STRPRINTF("Incomplete: %s: %s\n", + sexpr::token_name(lexer.peek()), lexer.val_string())); + } + return 0; + } + + std::stack<Spacing> spacing; + spacing.push(LINES); + sexpr::Lexer lexer("/dev/stdin"); + bool first = true; + while (sexpr::Lexeme tok = lexer.peek()) + { + switch (tok) + { + case sexpr::TOK_OPEN: + if (spacing.top() == SIMPLE) + spacing.top() = LINES; + do_spacing(first, spacing.top(), spacing.size()); + PRINTF("("); + spacing.push(SIMPLE); + first = true; + break; + case sexpr::TOK_CLOSE: + PRINTF(")"); + spacing.pop(); + first = false; + break; + case sexpr::TOK_STRING: + do_spacing(first, spacing.top(), spacing.size()); + PRINTF("%s", sexpr::escape(lexer.val_string())); + break; + case sexpr::TOK_TOKEN: + do_spacing(first, spacing.top(), spacing.size()); + PRINTF("%s", lexer.val_string()); + adjust_spacing(spacing.top(), lexer.val_string()); + break; + default: + abort(); + } + lexer.adv(); + } + PRINTF("\n"); +} diff --git a/src/sexpr/parser.cpp b/src/sexpr/parser.cpp new file mode 100644 index 0000000..2068565 --- /dev/null +++ b/src/sexpr/parser.cpp @@ -0,0 +1,79 @@ +#include "parser.hpp" +// parser.cpp - build a tree of S-expressions +// +// 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 sexpr +{ + bool token_is_int(ZString s, int64_t& out, bool& ok) + { + if (!s) + return false; + if (s.startswith('-') || s.xslice_h(1).is_digit10()) + { + const char *z = s.c_str(); + char *end = nullptr; + errno = 0; + out = strtoll(z, &end, 0); + if (errno) + ok = false; + return !*end; + } + return false; + } + + bool parse(Lexer& lex, SExpr& out) + { + out._list.clear(); + out._str = RString(); + + bool rv = true; + out._span.begin = lex.span().begin; + switch (lex.peek()) + { + default: + return false; + case TOK_STRING: + out._type = STRING; + out._str = lex.val_string(); + break; + case TOK_TOKEN: + out._type = TOKEN; + out._str = lex.val_string(); + if (token_is_int(out._str, out._int, rv)) + out._type = INT; + break; + case TOK_OPEN: + out._type = LIST; + lex.adv(); + while (lex.peek() != TOK_CLOSE) + { + SExpr tmp; + if (!parse(lex, tmp)) + return false; + out._list.push_back(std::move(tmp)); + } + break; + } + out._span.end = lex.span().end; + lex.adv(); + return rv; + } +} // namespace sexpr diff --git a/src/sexpr/parser.hpp b/src/sexpr/parser.hpp new file mode 100644 index 0000000..6097f78 --- /dev/null +++ b/src/sexpr/parser.hpp @@ -0,0 +1,80 @@ +#ifndef TMWA_SEXPR_PARSER_HPP +#define TMWA_SEXPR_PARSER_HPP +// parser.hpp - build a tree of S-expressions +// +// 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 "../sanity.hpp" + +# include "../strings/zstring.hpp" + +# include "../io/line.hpp" + +# include "lexer.hpp" + +namespace sexpr +{ + enum Type + { + LIST, + INT, + STRING, + TOKEN, + }; + + struct SExpr + { + Type _type; + int64_t _int; + RString _str; + std::vector<SExpr> _list; + + io::LineSpan _span; + + SExpr() : _type(), _int(), _str(), _list(), _span() {} + }; + + inline + bool operator == (const SExpr& l, const SExpr& r) + { + if (l._type != r._type) + return false; + switch (l._type) + { + case LIST: + return l._list == r._list; + case INT: + return l._int == r._int; + case STRING: + case TOKEN: + return l._str == r._str; + } + abort(); + } + inline + bool operator != (const SExpr& l, const SExpr& r) + { + return !(l == r); + } + + bool token_is_int(ZString s, int64_t& out, bool& ok); + /// return false on error or eof, check lex.peek() == TOK_EOF to see + bool parse(Lexer& lex, SExpr& out); +} // namespace sexpr + +#endif // TMWA_SEXPR_PARSER_HPP diff --git a/src/sexpr/parser.py b/src/sexpr/parser.py new file mode 100644 index 0000000..d638259 --- /dev/null +++ b/src/sexpr/parser.py @@ -0,0 +1,25 @@ +class SExpr(object): + ''' print a SExpr + ''' + __slots__ = ('_value') + name = 'sexpr::SExpr' + enabled = True + + def __init__(self, value): + self._value = value + + def to_string(self): + return None + + def children(self): + v = self._value + t = v['_type'] + if t == 0: + yield '(list)', v['_list'] + if t == 1: + yield '(int)', v['_int'] + if t == 2: + yield '(str)', v['_str'] + if t == 3: + yield '(token)', v['_str'] + yield '_span', v['_span'] diff --git a/src/sexpr/parser_test.cpp b/src/sexpr/parser_test.cpp new file mode 100644 index 0000000..a752313 --- /dev/null +++ b/src/sexpr/parser_test.cpp @@ -0,0 +1,89 @@ +#include "parser.hpp" + +#include <gtest/gtest.h> + +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; +} + +TEST(sexpr, parser) +{ + sexpr::SExpr s; + io::LineSpan span; + sexpr::Lexer lexer("<parser-test1>", string_pipe(" foo( ) 123\"\" \n")); + + EXPECT_TRUE(sexpr::parse(lexer, s)); + EXPECT_EQ(s._type, sexpr::TOKEN); + EXPECT_EQ(s._str, "foo"); + + EXPECT_TRUE(sexpr::parse(lexer, s)); + EXPECT_EQ(s._type, sexpr::LIST); + EXPECT_EQ(s._list, std::vector<sexpr::SExpr>()); + + EXPECT_TRUE(sexpr::parse(lexer, s)); + EXPECT_EQ(s._type, sexpr::INT); + EXPECT_EQ(s._int, 123); + + EXPECT_TRUE(sexpr::parse(lexer, s)); + EXPECT_EQ(s._type, sexpr::STRING); + EXPECT_EQ(s._str, ""); + + EXPECT_FALSE(sexpr::parse(lexer, s)); + EXPECT_EQ(lexer.peek(), sexpr::TOK_EOF); +} + +TEST(sexpr, parselist) +{ + sexpr::SExpr s; + sexpr::Lexer lexer("<parser-test1>", string_pipe("(foo)(bar)\n")); + + EXPECT_TRUE(sexpr::parse(lexer, s)); + EXPECT_EQ(s._type, sexpr::LIST); + EXPECT_EQ(s._list.size(), 1); + EXPECT_EQ(s._list[0]._type, sexpr::TOKEN); + EXPECT_EQ(s._list[0]._str, "foo"); + + EXPECT_TRUE(sexpr::parse(lexer, s)); + EXPECT_EQ(s._type, sexpr::LIST); + EXPECT_EQ(s._list.size(), 1); + EXPECT_EQ(s._list[0]._type, sexpr::TOKEN); + EXPECT_EQ(s._list[0]._str, "bar"); + + EXPECT_FALSE(sexpr::parse(lexer, s)); + EXPECT_EQ(lexer.peek(), sexpr::TOK_EOF); +} + +TEST(sexpr, parsebad) +{ + for (ZString bad : { + ZString("(\n"), + ZString(")\n"), + ZString("\"\n"), + ZString("'\n"), + ZString("\\\n"), + ZString("\"\\"), + ZString("\"\\z\""), + ZString("(()\n"), + ZString("((\n"), + ZString("((\"\n"), + }) + { + sexpr::SExpr s; + io::LineSpan span; + sexpr::Lexer lexer("<parse-bad>", string_pipe(bad)); + EXPECT_FALSE(sexpr::parse(lexer, s)); + EXPECT_EQ(lexer.peek(), sexpr::TOK_ERROR); + } +} |