From 1a00fe4ea75924bfe594c4d92073cc95eaa2f32d Mon Sep 17 00:00:00 2001
From: Ben Longbons <b.r.longbons@gmail.com>
Date: Wed, 29 Oct 2014 14:56:55 -0700
Subject: Item AST

---
 src/ast/item.cpp          | 160 ++++++++++++++++++++++++++++++++++++++++++++++
 src/ast/item.hpp          |  80 +++++++++++++++++++++++
 src/ast/item_test.cpp     | 150 +++++++++++++++++++++++++++++++++++++++++++
 src/ast/npc.cpp           |  53 ++++++++-------
 src/ast/npc.hpp           |  10 +--
 src/ast/npc_test.cpp      |  82 +++++++++++++-----------
 src/ast/script.cpp        |  14 ++--
 src/ast/script.hpp        |  24 +++++--
 src/io/fwd.hpp            |   2 +
 src/io/line.cpp           | 107 ++++++++++++-------------------
 src/io/line.hpp           |  59 ++---------------
 src/io/line_test.cpp      |  50 +++++++++++++++
 src/io/read.cpp           |  38 ++++++++++-
 src/io/read.hpp           |  14 +++-
 src/io/read_test.cpp      |  61 ++++++++++++++++++
 src/io/span.cpp           |  99 ++++++++++++++++++++++++++++
 src/io/span.hpp           |  90 ++++++++++++++++++++++++++
 src/main-gdb-head.py      |   2 +-
 src/sexpr/lexer.hpp       |   9 ++-
 src/sexpr/lexer_test.cpp  |  22 +------
 src/sexpr/parser_test.cpp |  22 +------
 21 files changed, 905 insertions(+), 243 deletions(-)
 create mode 100644 src/ast/item.cpp
 create mode 100644 src/ast/item.hpp
 create mode 100644 src/ast/item_test.cpp
 create mode 100644 src/io/span.cpp
 create mode 100644 src/io/span.hpp

diff --git a/src/ast/item.cpp b/src/ast/item.cpp
new file mode 100644
index 0000000..99417d8
--- /dev/null
+++ b/src/ast/item.cpp
@@ -0,0 +1,160 @@
+#include "item.hpp"
+//    ast/item.cpp - Structure of itemdb
+//
+//    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 "../compat/memory.hpp"
+
+#include "../io/extract.hpp"
+#include "../io/line.hpp"
+
+#include "../mmo/extract_enums.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace item
+{
+    using io::respan;
+
+    // separate file because virtual
+    ItemOrComment::~ItemOrComment() {}
+
+    static
+    void skip_comma_space(io::LineCharReader& lr)
+    {
+        io::LineChar c;
+        if (lr.get(c) && c.ch() == ',')
+        {
+            lr.adv();
+            while (lr.get(c) && c.ch() == ' ')
+            {
+                lr.adv();
+            }
+        }
+    }
+    static
+    Option<Spanned<RString>> lex_nonscript(io::LineCharReader& lr, bool first)
+    {
+        io::LineChar c;
+        if (first)
+        {
+            while (lr.get(c) && c.ch() == '\n')
+            {
+                lr.adv();
+            }
+        }
+        if (!lr.get(c))
+        {
+            return None;
+        }
+        io::LineSpan span;
+        MString accum;
+        accum += c.ch();
+        span.begin = c;
+        span.end = c;
+        lr.adv();
+        if (c.ch() != '/')
+            first = false;
+
+        if (first && lr.get(c) && c.ch() == '/')
+        {
+            accum += c.ch();
+            span.end = c;
+            lr.adv();
+            while (lr.get(c) && c.ch() != '\n')
+            {
+                accum += c.ch();
+                span.end = c;
+                lr.adv();
+            }
+            return Some(respan(span, RString(accum)));
+        }
+
+        while (lr.get(c) && c.ch() != ',' && c.ch() != '\n')
+        {
+            accum += c.ch();
+            span.end = c;
+            lr.adv();
+        }
+        skip_comma_space(lr);
+        return Some(respan(span, RString(accum)));
+    }
+
+    static
+    Result<ast::script::ScriptBody> lex_script(io::LineCharReader& lr)
+    {
+        ast::script::ScriptOptions opt;
+        opt.implicit_start = true;
+        opt.implicit_end = true;
+        opt.one_line = true;
+        auto rv = ast::script::parse_script_body(lr, opt);
+        if (rv.get_success().is_some())
+        {
+            skip_comma_space(lr);
+        }
+        return rv;
+    }
+
+#define SPAN_EXTRACT(bitexpr, var) ({ auto bit = bitexpr; if (!extract(bit.data, &var.data)) return Err(bit.span.error_str("failed to extract"_s #var)); var.span = bit.span; })
+
+#define EOL_ERROR(lr) ({ io::LineChar c; lr.get(c) ? Err(c.error_str("unexpected EOL"_s)) : Err("unexpected EOF before unexpected EOL"_s); })
+    Result<std::unique_ptr<ItemOrComment>> parse_item(io::LineCharReader& lr)
+    {
+        std::unique_ptr<ItemOrComment> rv = nullptr;
+        Spanned<RString> first = TRY_UNWRAP(lex_nonscript(lr, true), return Ok(std::move(rv)));
+        if (first.data.startswith("//"_s))
+        {
+            Comment comment;
+            comment.span = first.span;
+            comment.comment = first.data;
+            rv = make_unique<Comment>(std::move(comment));
+            return Ok(std::move(rv));
+        }
+        Item item;
+        SPAN_EXTRACT(first, item.id);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.name);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.jname);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.type);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.buy_price);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.sell_price);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.weight);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.atk);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.def);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.range);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.magic_bonus);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.slot_unused);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.gender);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.loc);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.wlv);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.elv);
+        SPAN_EXTRACT(TRY_UNWRAP(lex_nonscript(lr, false), return EOL_ERROR(lr)), item.view);
+        item.use_script = TRY(lex_script(lr));
+        item.equip_script = TRY(lex_script(lr));
+        item.span.begin = item.id.span.begin;
+        item.span.end = item.equip_script.span.end;
+        rv = make_unique<Item>(std::move(item));
+        return Ok(std::move(rv));
+    }
+} // namespace item
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/item.hpp b/src/ast/item.hpp
new file mode 100644
index 0000000..b54e55c
--- /dev/null
+++ b/src/ast/item.hpp
@@ -0,0 +1,80 @@
+#pragma once
+//    ast/item.hpp - Structure of tmwa itemdb
+//
+//    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 <memory>
+
+#include "../compat/result.hpp"
+
+#include "../io/span.hpp"
+
+#include "../mmo/clif.t.hpp"
+#include "../mmo/ids.hpp"
+#include "../mmo/strs.hpp"
+
+#include "script.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace item
+{
+    using io::Spanned;
+
+    struct ItemOrComment
+    {
+        io::LineSpan span;
+
+        virtual ~ItemOrComment();
+    };
+    struct Comment : ItemOrComment
+    {
+        RString comment;
+    };
+    struct Item : ItemOrComment
+    {
+        Spanned<ItemNameId> id;
+        Spanned<ItemName> name;
+        Spanned<ItemName> jname;
+        Spanned<ItemType> type;
+        Spanned<int> buy_price;
+        Spanned<int> sell_price;
+        Spanned<int> weight;
+        Spanned<int> atk;
+        Spanned<int> def;
+        Spanned<int> range;
+        Spanned<int> magic_bonus;
+        Spanned<RString> slot_unused;
+        Spanned<SEX> gender;
+        Spanned<EPOS> loc;
+        Spanned<int> wlv;
+        Spanned<int> elv;
+        Spanned<ItemLook> view;
+        ast::script::ScriptBody use_script;
+        ast::script::ScriptBody equip_script;
+    };
+
+    Result<std::unique_ptr<ItemOrComment>> parse_item(io::LineCharReader& lr);
+} // namespace item
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/item_test.cpp b/src/ast/item_test.cpp
new file mode 100644
index 0000000..3b5fd07
--- /dev/null
+++ b/src/ast/item_test.cpp
@@ -0,0 +1,150 @@
+#include "item.hpp"
+//    ast/item_test.cpp - Testsuite for itemdb 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 "../io/line.hpp"
+
+#include "../tests/fdhack.hpp"
+
+//#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace ast
+{
+namespace item
+{
+#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(itemast, eof)
+    {
+        QuietFd q;
+        LString inputs[] =
+        {
+            ""_s,
+            "\n"_s,
+            "\n\n"_s,
+        };
+        for (auto input : inputs)
+        {
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
+            auto res = parse_item(lr);
+            EXPECT_EQ(res.get_success(), Some(std::unique_ptr<ItemOrComment>(nullptr)));
+        }
+    }
+    TEST(itemast, comment)
+    {
+        QuietFd q;
+        LString inputs[] =
+        {
+            //23456789
+            "// hello"_s,
+            "// hello\n "_s,
+            "// hello\nabc"_s,
+        };
+        for (auto input : inputs)
+        {
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
+            auto res = parse_item(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(itemast, item)
+    {
+        QuietFd q;
+        LString inputs[] =
+        {
+            //        1         2         3         4         5
+            //2345678901234567890123456789012345678901234567890123456789
+            "1,abc ,  def,3,4,5,6,7,8,9,10,xx,2,16,12,13,11, {end;}, {}"_s,
+            "1,abc ,  def,3,4,5,6,7,8,9,10,xx,2,16,12,13,11, {end;}, {}\n"_s,
+            "1,abc ,  def,3,4,5,6,7,8,9,10,xx,2,16,12,13,11, {end;}, {}\nabc"_s,
+        };
+        for (auto input : inputs)
+        {
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
+            auto res = parse_item(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,58);
+            auto p = dynamic_cast<Item *>(top.get());
+            EXPECT_TRUE(p);
+            if (p)
+            {
+                EXPECT_SPAN(p->id.span, 1,1, 1,1);
+                EXPECT_EQ(p->id.data, wrap<ItemNameId>(1));
+                EXPECT_SPAN(p->name.span, 1,3, 1,6);
+                EXPECT_EQ(p->name.data, stringish<ItemName>("abc "_s));
+                EXPECT_SPAN(p->jname.span, 1,10, 1,12);
+                EXPECT_EQ(p->jname.data, stringish<ItemName>("def"_s));
+                EXPECT_SPAN(p->type.span, 1,14, 1,14);
+                EXPECT_EQ(p->type.data, ItemType::JUNK);
+                EXPECT_SPAN(p->buy_price.span, 1,16, 1,16);
+                EXPECT_EQ(p->buy_price.data, 4);
+                EXPECT_SPAN(p->sell_price.span, 1,18, 1,18);
+                EXPECT_EQ(p->sell_price.data, 5);
+                EXPECT_SPAN(p->weight.span, 1,20, 1,20);
+                EXPECT_EQ(p->weight.data, 6);
+                EXPECT_SPAN(p->atk.span, 1,22, 1,22);
+                EXPECT_EQ(p->atk.data, 7);
+                EXPECT_SPAN(p->def.span, 1,24, 1,24);
+                EXPECT_EQ(p->def.data, 8);
+                EXPECT_SPAN(p->range.span, 1,26, 1,26);
+                EXPECT_EQ(p->range.data, 9);
+                EXPECT_SPAN(p->magic_bonus.span, 1,28, 1,29);
+                EXPECT_EQ(p->magic_bonus.data, 10);
+                EXPECT_SPAN(p->slot_unused.span, 1,31, 1,32);
+                EXPECT_EQ(p->slot_unused.data, "xx"_s);
+                EXPECT_SPAN(p->gender.span, 1,34, 1,34);
+                EXPECT_EQ(p->gender.data, SEX::NEUTRAL);
+                EXPECT_SPAN(p->loc.span, 1,36, 1,37);
+                EXPECT_EQ(p->loc.data, EPOS::MISC1);
+                EXPECT_SPAN(p->wlv.span, 1,39, 1,40);
+                EXPECT_EQ(p->wlv.data, 12);
+                EXPECT_SPAN(p->elv.span, 1,42, 1,43);
+                EXPECT_EQ(p->elv.data, 13);
+                EXPECT_SPAN(p->view.span, 1,45, 1,46);
+                EXPECT_EQ(p->view.data, ItemLook::BOW);
+                EXPECT_SPAN(p->use_script.span, 1,49, 1,54);
+                EXPECT_EQ(p->use_script.braced_body, "{end;}"_s);
+                EXPECT_SPAN(p->equip_script.span, 1,57, 1,58);
+                EXPECT_EQ(p->equip_script.braced_body, "{}"_s);
+            }
+        }
+    }
+} // namespace item
+} // namespace ast
+} // namespace tmwa
diff --git a/src/ast/npc.cpp b/src/ast/npc.cpp
index ca518d8..ceb381d 100644
--- a/src/ast/npc.cpp
+++ b/src/ast/npc.cpp
@@ -22,6 +22,7 @@
 
 #include "../io/cxxstdio.hpp"
 #include "../io/extract.hpp"
+#include "../io/line.hpp"
 
 #include "../mmo/extract_enums.hpp"
 
@@ -32,10 +33,12 @@
 
 namespace tmwa
 {
-namespace npc
+namespace ast
 {
-namespace parse
+namespace npc
 {
+    using io::respan;
+
     // separate file because virtual
     TopLevel::~TopLevel() {}
 
@@ -285,7 +288,10 @@ namespace parse
         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));
+        ast::script::ScriptOptions opt;
+        opt.implicit_start = true;
+        opt.default_label = "OnCall"_s;
+        script_function.body = TRY(ast::script::parse_script_body(lr, opt));
         return Ok(std::move(script_function));
     }
     static
@@ -317,7 +323,10 @@ namespace parse
         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));
+        ast::script::ScriptOptions opt;
+        opt.implicit_start = true;
+        opt.no_start = true;
+        script_none.body = TRY(ast::script::parse_script_body(lr, opt));
         return Ok(std::move(script_none));
     }
     static
@@ -353,7 +362,10 @@ namespace parse
         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));
+        ast::script::ScriptOptions opt;
+        opt.implicit_start = true;
+        opt.no_start = true;
+        script_map_none.body = TRY(ast::script::parse_script_body(lr, opt));
         return Ok(std::move(script_map_none));
     }
     static
@@ -391,7 +403,10 @@ namespace parse
         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));
+        ast::script::ScriptOptions opt;
+        opt.implicit_start = true;
+        opt.default_label = "OnClick"_s;
+        script_map.body = TRY(ast::script::parse_script_body(lr, opt));
         return Ok(std::move(script_map));
     }
     static
@@ -428,6 +443,9 @@ namespace parse
     static
     Option<Spanned<RString>> lex(io::LineCharReader& lr, bool first)
     {
+        // you know, I just realized a lot of the if (.get()) checks are not
+        // actually going to fail, since LineCharReader guarantees the \n
+        // occurs before EOF
         io::LineChar c;
         // at start of line, skip whitespace
         if (first)
@@ -445,12 +463,8 @@ namespace parse
         // 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);
+            return Some(respan({c, c}, RString(VString<1>(c.ch()))));
         }
         io::LineSpan span;
         MString accum;
@@ -464,10 +478,7 @@ namespace parse
         // 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);
+            return Some(respan(span, RString(accum)));
         }
 
         accum += c.ch();
@@ -483,10 +494,7 @@ namespace parse
                 span.end = c;
                 lr.adv();
             }
-            Spanned<RString> bit;
-            bit.span = span;
-            bit.data = RString(accum);
-            return Some(bit);
+            return Some(respan(span, RString(accum)));
         }
         // otherwise, collect until an end of line or separator
         while (lr.get(c) && c.ch() != '\n' && c.ch() != '{' && c.ch() != ',' && c.ch() != '|')
@@ -495,10 +503,7 @@ namespace parse
             span.end = c;
             lr.adv();
         }
-        Spanned<RString> bit;
-        bit.span = span;
-        bit.data = RString(accum);
-        return Some(std::move(bit));
+        return Some(respan(span, RString(accum)));
     }
 
     Result<std::unique_ptr<TopLevel>> parse_top(io::LineCharReader& in)
@@ -592,6 +597,6 @@ namespace parse
         }
         return Ok(std::move(rv));
     }
-} // namespace parse
 } // namespace npc
+} // namespace ast
 } // namespace tmwa
diff --git a/src/ast/npc.hpp b/src/ast/npc.hpp
index 648b40b..dd323b6 100644
--- a/src/ast/npc.hpp
+++ b/src/ast/npc.hpp
@@ -24,6 +24,8 @@
 
 #include "../compat/result.hpp"
 
+#include "../io/span.hpp"
+
 #include "../mmo/clif.t.hpp"
 #include "../mmo/ids.hpp"
 #include "../mmo/strs.hpp"
@@ -35,9 +37,9 @@
 
 namespace tmwa
 {
-namespace npc
+namespace ast
 {
-namespace parse
+namespace npc
 {
     using io::Spanned;
 
@@ -100,7 +102,7 @@ namespace parse
     {
         io::LineSpan key_span;
         // see src/script/parser.hpp
-        script::parse::ScriptBody body;
+        ast::script::ScriptBody body;
     };
     struct ScriptFunction : Script
     {
@@ -133,6 +135,6 @@ namespace parse
     // other Script subclasses elsewhere? (for item and magic scripts)
 
     Result<std::unique_ptr<TopLevel>> parse_top(io::LineCharReader& in);
-} // namespace parse
 } // namespace npc
+} // namespace ast
 } // namespace tmwa
diff --git a/src/ast/npc_test.cpp b/src/ast/npc_test.cpp
index 2697351..ea4bdf3 100644
--- a/src/ast/npc_test.cpp
+++ b/src/ast/npc_test.cpp
@@ -20,6 +20,8 @@
 
 #include <gtest/gtest.h>
 
+#include "../io/line.hpp"
+
 #include "../tests/fdhack.hpp"
 
 //#include "../poison.hpp"
@@ -27,26 +29,10 @@
 
 namespace tmwa
 {
-namespace npc
+namespace ast
 {
-namespace parse
+namespace npc
 {
-    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);   \
@@ -55,7 +41,7 @@ namespace parse
         EXPECT_EQ((span).end.column, ec);   \
     })
 
-    TEST(ast, eof)
+    TEST(npcast, eof)
     {
         QuietFd q;
         LString inputs[] =
@@ -66,12 +52,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_EQ(res.get_success(), Some(std::unique_ptr<TopLevel>(nullptr)));
         }
     }
-    TEST(ast, comment)
+    TEST(npcast, comment)
     {
         QuietFd q;
         LString inputs[] =
@@ -83,10 +69,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,8);
             auto p = dynamic_cast<Comment *>(top.get());
             EXPECT_TRUE(p);
@@ -96,7 +84,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, warp)
+    TEST(npcast, warp)
     {
         QuietFd q;
         LString inputs[] =
@@ -110,10 +98,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,47);
             auto p = dynamic_cast<Warp *>(top.get());
             EXPECT_TRUE(p);
@@ -141,7 +131,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, shop)
+    TEST(npcast, shop)
     {
         QuietFd q;
         LString inputs[] =
@@ -155,10 +145,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,54);
             auto p = dynamic_cast<Shop *>(top.get());
             EXPECT_TRUE(p);
@@ -197,7 +189,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, monster)
+    TEST(npcast, monster)
     {
         QuietFd q;
         LString inputs[] =
@@ -220,10 +212,12 @@ namespace parse
             bool second = input.startswith('M');
             bool third = input.startswith('n');
             assert(first + second + third == 1);
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,first?65:54);
             auto p = dynamic_cast<Monster *>(top.get());
             EXPECT_TRUE(p);
@@ -302,7 +296,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, mapflag)
+    TEST(npcast, mapflag)
     {
         QuietFd q;
         LString inputs[] =
@@ -319,10 +313,12 @@ namespace parse
         for (auto input : inputs)
         {
             bool second = input.startswith('M');
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,!second?24:31);
             auto p = dynamic_cast<MapFlag *>(top.get());
             EXPECT_TRUE(p);
@@ -354,7 +350,7 @@ namespace parse
         }
     }
 
-    TEST(ast, scriptfun)
+    TEST(npcast, scriptfun)
     {
         QuietFd q;
         LString inputs[] =
@@ -369,10 +365,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,24);
             auto p = dynamic_cast<ScriptFunction *>(top.get());
             EXPECT_TRUE(p);
@@ -402,7 +400,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, scriptnone)
+    TEST(npcast, scriptnone)
     {
         QuietFd q;
         LString inputs[] =
@@ -417,10 +415,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,19);
             auto p = dynamic_cast<ScriptNone *>(top.get());
             EXPECT_TRUE(p);
@@ -451,7 +451,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, scriptmapnone)
+    TEST(npcast, scriptmapnone)
     {
         QuietFd q;
         LString inputs[] =
@@ -464,10 +464,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,28);
             auto p = dynamic_cast<ScriptMapNone *>(top.get());
             EXPECT_TRUE(p);
@@ -505,7 +507,7 @@ namespace parse
             }
         }
     }
-    TEST(ast, scriptmap)
+    TEST(npcast, scriptmap)
     {
         QuietFd q;
         LString inputs[] =
@@ -518,10 +520,12 @@ namespace parse
         };
         for (auto input : inputs)
         {
-            io::LineCharReader lr("<string>"_s, string_pipe(input));
+            io::LineCharReader lr(io::from_string, "<string>"_s, input);
             auto res = parse_top(lr);
             EXPECT_TRUE(res.get_success().is_some());
             auto top = TRY_UNWRAP(std::move(res.get_success()), FAIL());
+            if (!top)
+                FAIL();
             EXPECT_SPAN(top->span, 1,1, 1,31);
             auto p = dynamic_cast<ScriptMap *>(top.get());
             EXPECT_TRUE(p);
@@ -564,6 +568,6 @@ namespace parse
             }
         }
     }
-} // namespace parse
 } // namespace npc
+} // namespace ast
 } // namespace tmwa
diff --git a/src/ast/script.cpp b/src/ast/script.cpp
index cc67224..ec958e1 100644
--- a/src/ast/script.cpp
+++ b/src/ast/script.cpp
@@ -18,16 +18,18 @@
 //    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 "../io/line.hpp"
+
 #include "../poison.hpp"
 
 
 namespace tmwa
 {
-namespace script
+namespace ast
 {
-namespace parse
+namespace script
 {
-    Result<ScriptBody> parse_script_body(io::LineCharReader& lr)
+    Result<ScriptBody> parse_script_body(io::LineCharReader& lr, ScriptOptions opt)
     {
         io::LineSpan span;
         io::LineChar c;
@@ -37,7 +39,7 @@ namespace parse
             {
                 return Err("error: unexpected EOF before '{' in parse_script_body"_s);
             }
-            if (c.ch() == ' ' || c.ch() == '\n')
+            if (c.ch() == ' ' || (!opt.one_line && c.ch() == '\n'))
             {
                 lr.adv();
                 continue;
@@ -57,6 +59,8 @@ namespace parse
         {
             if (!lr.get(c))
                 return Err(c.error_str("unexpected EOF before '}' in parse_script_body"_s));
+            if (opt.one_line && c.ch() == '\n')
+                return Err(c.error_str("unexpected EOL before '}' in parse_script_body"_s));
             accum += c.ch();
             span.end = c;
             lr.adv();
@@ -66,6 +70,6 @@ namespace parse
             }
         }
     }
-} // namespace parse
 } // namespace script
+} // namespace ast
 } // namespace tmwa
diff --git a/src/ast/script.hpp b/src/ast/script.hpp
index 59e53f0..da43f90 100644
--- a/src/ast/script.hpp
+++ b/src/ast/script.hpp
@@ -22,14 +22,14 @@
 
 #include "../compat/result.hpp"
 
-#include "../io/line.hpp"
+#include "../io/span.hpp"
 
 
 namespace tmwa
 {
-namespace script
+namespace ast
 {
-namespace parse
+namespace script
 {
     using io::Spanned;
 
@@ -39,7 +39,21 @@ namespace parse
         io::LineSpan span;
     };
 
-    Result<ScriptBody> parse_script_body(io::LineCharReader& lr);
+    struct ScriptOptions
+    {
+        // don't require a label at the beginning
+        bool implicit_start = false;
+        // label to generate at the beginning if not already present
+        RString default_label;
+        // beginning must be only 'end;'
+        bool no_start;
+        // don't requite an 'end;' at the end
+        bool implicit_end = false;
+        // forbid newlines anywhere between { and }
+        bool one_line = false;
+    };
+
+    Result<ScriptBody> parse_script_body(io::LineCharReader& lr, ScriptOptions opt);
 
     /*
     (-- First bare-body-chunk only allowed for npcs, items, magic, functions.
@@ -89,6 +103,6 @@ namespace parse
     simple-expr: variable ("[" expr "]")?
     simple-expr: function // no longer command/label though
     */
-} // namespace parse
 } // namespace script
+} // namespace ast
 } // namespace tmwa
diff --git a/src/io/fwd.hpp b/src/io/fwd.hpp
index 99268f4..5334fbd 100644
--- a/src/io/fwd.hpp
+++ b/src/io/fwd.hpp
@@ -34,5 +34,7 @@ namespace io
     class ReadFile;
     class WriteFile;
     class AppendFile;
+    class LineReader;
+    class LineCharReader;
 } // namespace io
 } // namespace tmwa
diff --git a/src/io/line.cpp b/src/io/line.cpp
index a1cdf42..5d7e792 100644
--- a/src/io/line.cpp
+++ b/src/io/line.cpp
@@ -19,9 +19,7 @@
 //    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 #include "../strings/astring.hpp"
-#include "../strings/mstring.hpp"
 #include "../strings/zstring.hpp"
-#include "../strings/xstring.hpp"
 
 #include "cxxstdio.hpp"
 
@@ -32,71 +30,6 @@ namespace tmwa
 {
 namespace io
 {
-    AString Line::message_str(ZString cat, ZString msg) const
-    {
-        MString out;
-        if (column)
-            out += STRPRINTF("%s:%u:%u: %s: %s\n"_fmt,
-                    filename, line, column, cat, msg);
-        else
-            out += STRPRINTF("%s:%u: %s: %s\n"_fmt,
-                    filename, line, cat, msg);
-        out += STRPRINTF("%s\n"_fmt, text);
-        out += STRPRINTF("%*c\n"_fmt, column, '^');
-        return AString(out);
-    }
-
-    void Line::message(ZString cat, ZString msg) const
-    {
-        if (column)
-            FPRINTF(stderr, "%s:%u:%u: %s: %s\n"_fmt,
-                    filename, line, column, cat, msg);
-        else
-            FPRINTF(stderr, "%s:%u: %s: %s\n"_fmt,
-                    filename, line, cat, msg);
-        FPRINTF(stderr, "%s\n"_fmt, text);
-        FPRINTF(stderr, "%*c\n"_fmt, column, '^');
-    }
-
-    AString LineSpan::message_str(ZString cat, ZString msg) const
-    {
-        assert (begin.column);
-        assert (end.column);
-        assert (begin.line < end.line || begin.column <= end.column);
-
-        MString out;
-        if (begin.line == end.line)
-        {
-            out += STRPRINTF("%s:%u:%u: %s: %s\n"_fmt,
-                    begin.filename, begin.line, begin.column, cat, msg);
-            out += STRPRINTF("%s\n"_fmt, begin.text);
-            out += STRPRINTF("%*c"_fmt, begin.column, '^');
-            for (unsigned c = begin.column; c != end.column; ++c)
-                out += '~';
-            out += '\n';
-        }
-        else
-        {
-            out += STRPRINTF("%s:%u:%u: %s: %s\n"_fmt,
-                    begin.filename, begin.line, begin.column, cat, msg);
-            out += STRPRINTF("%s\n"_fmt, begin.text);
-            out += STRPRINTF("%*c"_fmt, begin.column, '^');
-            for (unsigned c = begin.column; c != begin.text.size(); ++c)
-                out += '~';
-            out += " ...\n"_s;
-            out += STRPRINTF("%s\n"_fmt, end.text);
-            for (unsigned c = 0; c != end.column; ++c)
-                out += '~';
-            out += '\n';
-        }
-        return AString(out);
-    }
-
-    void LineSpan::message(ZString cat, ZString msg) const
-    {
-        FPRINTF(stderr, "%s"_fmt, message_str(cat, msg));
-    }
-
     LineReader::LineReader(ZString name)
     : filename(name), line(0), column(0), rf(name)
     {}
@@ -105,6 +38,14 @@ namespace io
     : filename(name), line(0), column(0), rf(fd)
     {}
 
+    LineReader::LineReader(read_file_from_string, ZString name, XString content, int startline, FD fd)
+    : filename(name), line(startline-1), column(0), rf(from_string, content, fd)
+    {}
+
+    LineReader::LineReader(read_file_from_string, ZString name, LString content, int startline, FD fd)
+    : filename(name), line(startline-1), column(0), rf(from_string, content, fd)
+    {}
+
     bool LineReader::read_line(Line& l)
     {
         AString text;
@@ -145,6 +86,38 @@ namespace io
             column = 0;
     }
 
+    LineCharReader::LineCharReader(read_file_from_string, ZString name, XString content, int startline, int startcol, FD fd)
+    : LineReader(from_string, name, content, 1, fd)
+    {
+        column = 1; // not 0, not whole line
+        if (rf.is_open())
+            adv();
+        if (!line)
+            column = 0;
+        else
+        {
+            line = startline;
+            column = startcol;
+            line_text = STRPRINTF("%*s"_fmt, static_cast<int>(column-1 + line_text.size()), line_text);
+        }
+    }
+
+    LineCharReader::LineCharReader(read_file_from_string, ZString name, LString content, int startline, int startcol, FD fd)
+    : LineReader(from_string, name, content, 1, fd)
+    {
+        column = 1; // not 0, not whole line
+        if (rf.is_open())
+            adv();
+        if (!line)
+            column = 0;
+        else
+        {
+            line = startline;
+            column = startcol;
+            line_text = STRPRINTF("%*s"_fmt, static_cast<int>(column-1 + line_text.size()), line_text);
+        }
+    }
+
     bool LineCharReader::get(LineChar& c)
     {
         if (!column)
diff --git a/src/io/line.hpp b/src/io/line.hpp
index 5572e98..c94eeb9 100644
--- a/src/io/line.hpp
+++ b/src/io/line.hpp
@@ -21,68 +21,15 @@
 #include "fwd.hpp"
 
 #include "../strings/rstring.hpp"
-#include "../strings/zstring.hpp"
-#include "../strings/literal.hpp"
 
 #include "read.hpp"
+#include "span.hpp"
 
 
 namespace tmwa
 {
 namespace io
 {
-    // TODO split this out
-    struct Line
-    {
-        RString text;
-
-        RString filename;
-        // 1-based
-        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); }
-        void error(ZString msg) const { message("error"_s, msg); }
-    };
-
-    // psst, don't tell anyone
-    struct LineChar : Line
-    {
-        char ch()
-        {
-            size_t c = column - 1;
-            if (c == text.size())
-                return '\n';
-            return text[c];
-        }
-    };
-
-    struct LineSpan
-    {
-        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:
@@ -96,6 +43,8 @@ namespace io
         LineReader& operator = (LineReader&&) = delete;
         // needed for unit tests
         LineReader(ZString name, FD fd);
+        LineReader(read_file_from_string, ZString name, XString content, int startline=1, FD fd=FD());
+        LineReader(read_file_from_string, ZString name, LString content, int startline=1, FD fd=FD());
 
         bool read_line(Line& l);
         bool is_open();
@@ -110,6 +59,8 @@ namespace io
         LineCharReader(LineCharReader&&) = delete;
         LineCharReader& operator = (LineCharReader&&) = delete;
         LineCharReader(ZString name, FD fd);
+        LineCharReader(read_file_from_string, ZString name, XString content, int startline=1, int startcol=1, FD fd=FD());
+        LineCharReader(read_file_from_string, ZString name, LString content, int startline=1, int startcol=1, FD fd=FD());
 
         bool get(LineChar& c);
         void adv();
diff --git a/src/io/line_test.cpp b/src/io/line_test.cpp
index 46e7b57..582ee81 100644
--- a/src/io/line_test.cpp
+++ b/src/io/line_test.cpp
@@ -130,6 +130,20 @@ TEST(io, line5)
     EXPECT_EQ(hi.column, 0);
     EXPECT_FALSE(lr.read_line(hi));
 }
+TEST(io, line1text)
+{
+    io::LineReader lr(io::from_string, "<string1text>"_s, "Hello\nWorld"_s, 2);
+    io::Line hi;
+    EXPECT_TRUE(lr.read_line(hi));
+    EXPECT_EQ(hi.text, "Hello"_s);
+    EXPECT_EQ(hi.line, 2);
+    EXPECT_EQ(hi.column, 0);
+    EXPECT_TRUE(lr.read_line(hi));
+    EXPECT_EQ(hi.text, "World"_s);
+    EXPECT_EQ(hi.line, 3);
+    EXPECT_EQ(hi.column, 0);
+    EXPECT_FALSE(lr.read_line(hi));
+}
 
 TEST(io, linechar1)
 {
@@ -382,6 +396,42 @@ TEST(io, linechar5)
     lr.adv();
     EXPECT_FALSE(lr.get(c));
 }
+TEST(io, linechar1text)
+{
+    io::LineCharReader lr(io::from_string, "<stringchar1text>"_s, "Hi\nWu\n"_s, 2, 3);
+    io::LineChar c;
+    EXPECT_TRUE(lr.get(c));
+    EXPECT_EQ(c.ch(), 'H');
+    EXPECT_EQ(c.line, 2);
+    EXPECT_EQ(c.column, 3);
+    lr.adv();
+    EXPECT_TRUE(lr.get(c));
+    EXPECT_EQ(c.ch(), 'i');
+    EXPECT_EQ(c.line, 2);
+    EXPECT_EQ(c.column, 4);
+    lr.adv();
+    EXPECT_TRUE(lr.get(c));
+    EXPECT_EQ(c.ch(), '\n');
+    EXPECT_EQ(c.line, 2);
+    EXPECT_EQ(c.column, 5);
+    lr.adv();
+    EXPECT_TRUE(lr.get(c));
+    EXPECT_EQ(c.ch(), 'W');
+    EXPECT_EQ(c.line, 3);
+    EXPECT_EQ(c.column, 1);
+    lr.adv();
+    EXPECT_TRUE(lr.get(c));
+    EXPECT_EQ(c.ch(), 'u');
+    EXPECT_EQ(c.line, 3);
+    EXPECT_EQ(c.column, 2);
+    lr.adv();
+    EXPECT_TRUE(lr.get(c));
+    EXPECT_EQ(c.ch(), '\n');
+    EXPECT_EQ(c.line, 3);
+    EXPECT_EQ(c.column, 3);
+    lr.adv();
+    EXPECT_FALSE(lr.get(c));
+}
 
 TEST(io, linespan)
 {
diff --git a/src/io/read.cpp b/src/io/read.cpp
index 30620a1..d701c7f 100644
--- a/src/io/read.cpp
+++ b/src/io/read.cpp
@@ -1,7 +1,7 @@
 #include "read.hpp"
 //    io/read.cpp - Input from files
 //
-//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//    Copyright © 2013-2014 Ben Longbons <b.r.longbons@gmail.com>
 //
 //    This file is part of The Mana World (Athena server)
 //
@@ -48,6 +48,32 @@ namespace io
     : fd(dir.open_fd(name, O_RDONLY | O_CLOEXEC)), start(0), end(0)
     {
     }
+    ReadFile::ReadFile(read_file_from_string, XString content, FD f)
+    : fd(f), start(0), end(), extra()
+    {
+        if (content.size() <= 4096)
+        {
+            end = content.size();
+            auto z = std::copy(content.begin(), content.end(), buf);
+            // only for debug sanity
+            std::fill(z, std::end(buf), 0);
+            return;
+        }
+        auto base = content.base();
+        if (!base)
+        {
+            extra = content;
+            end = content.size();
+            return;
+        }
+        start = &*content.begin() - &*base->begin();
+        end = &*content.end() - &*base->begin();
+        extra = *base;
+    }
+    ReadFile::ReadFile(read_file_from_string, LString content, FD f)
+    : ReadFile(from_string, RString(content), f)
+    {
+    }
     ReadFile::~ReadFile()
     {
         fd.close();
@@ -56,6 +82,14 @@ namespace io
 
     bool ReadFile::get(char& c)
     {
+        if (extra)
+        {
+            c = extra[start];
+            ++start;
+            if (start == end)
+                extra = ""_s;
+            return true;
+        }
         if (start == end)
         {
             if (fd == FD())
@@ -125,7 +159,7 @@ namespace io
 
     bool ReadFile::is_open()
     {
-        return fd != FD();
+        return fd != FD() || start != end;
     }
 } // namespace io
 } // namespace tmwa
diff --git a/src/io/read.hpp b/src/io/read.hpp
index c1c4882..2e3611b 100644
--- a/src/io/read.hpp
+++ b/src/io/read.hpp
@@ -1,7 +1,7 @@
 #pragma once
 //    io/read.hpp - Input from files.
 //
-//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//    Copyright © 2013-2014 Ben Longbons <b.r.longbons@gmail.com>
 //
 //    This file is part of The Mana World (Athena server)
 //
@@ -20,6 +20,8 @@
 
 #include "fwd.hpp"
 
+#include "../strings/rstring.hpp"
+
 #include "dir.hpp"
 #include "fd.hpp"
 
@@ -27,18 +29,28 @@ namespace tmwa
 {
 namespace io
 {
+    enum read_file_from_string
+    {
+        from_string,
+    };
+
+    // TODO - for internal warnings, it would be convenient if this class
+    // didn't exist at all, and instead everything was done with line info.
     class ReadFile
     {
     private:
         FD fd;
         unsigned short start, end;
         char buf[4096];
+        RString extra;
     public:
         explicit
         ReadFile(FD fd);
         explicit
         ReadFile(ZString name);
         ReadFile(const DirFd& dir, ZString name);
+        ReadFile(read_file_from_string, XString content, FD fd=FD());
+        ReadFile(read_file_from_string, LString content, FD fd=FD());
 
         ReadFile& operator = (ReadFile&&) = delete;
         ReadFile(ReadFile&&) = delete;
diff --git a/src/io/read_test.cpp b/src/io/read_test.cpp
index e655eb1..22c67c8 100644
--- a/src/io/read_test.cpp
+++ b/src/io/read_test.cpp
@@ -93,4 +93,65 @@ TEST(io, read5)
     EXPECT_FALSE(hi);
     EXPECT_FALSE(rf.getline(hi));
 }
+
+#define S15     "0123456789abcde"_s
+#define S16     "0123456789abcdef"_s
+#define S255    S16 S16 S16 S16  S16 S16 S16 S16   S16 S16 S16 S16  S16 S16 S16 S15
+#define S256    S16 S16 S16 S16  S16 S16 S16 S16   S16 S16 S16 S16  S16 S16 S16 S16
+#define S4095   S256 S256 S256 S256  S256 S256 S256 S256   S256 S256 S256 S256  S256 S256 S256 S255
+#define S4096   S256 S256 S256 S256  S256 S256 S256 S256   S256 S256 S256 S256  S256 S256 S256 S256
+
+TEST(io, readstringr)
+{
+    LString tests[] =
+    {
+        S15,
+        S16,
+        S255,
+        S256,
+        S4095,
+        S4096,
+        S4096 S16,
+    };
+    for (RString test : tests)
+    {
+        char buf[test.size() + 1];
+
+        io::ReadFile rf(io::from_string, test);
+        EXPECT_EQ(rf.get(buf, sizeof(buf)), test.size());
+        EXPECT_EQ(test, XString(buf + 0, buf + test.size(), nullptr));
+
+        io::ReadFile rf2(io::from_string, test, string_pipe("\na"_s));
+        EXPECT_EQ(rf2.get(buf, sizeof(buf)), test.size() + 1);
+        EXPECT_EQ(test, XString(buf + 0, buf + test.size(), nullptr));
+        EXPECT_EQ('\n', buf[test.size()]);
+    }
+}
+
+TEST(io, readstringx)
+{
+    LString tests[] =
+    {
+        S15,
+        S16,
+        S255,
+        S256,
+        S4095,
+        S4096,
+        S4096 S16,
+    };
+    for (XString test : tests)
+    {
+        char buf[test.size() + 1];
+
+        io::ReadFile rf(io::from_string, test);
+        EXPECT_EQ(rf.get(buf, sizeof(buf)), test.size());
+        EXPECT_EQ(test, XString(buf + 0, buf + test.size(), nullptr));
+
+        io::ReadFile rf2(io::from_string, test, string_pipe("\na"_s));
+        EXPECT_EQ(rf2.get(buf, sizeof(buf)), test.size() + 1);
+        EXPECT_EQ(test, XString(buf + 0, buf + test.size(), nullptr));
+        EXPECT_EQ('\n', buf[test.size()]);
+    }
+}
 } // namespace tmwa
diff --git a/src/io/span.cpp b/src/io/span.cpp
new file mode 100644
index 0000000..f4752f0
--- /dev/null
+++ b/src/io/span.cpp
@@ -0,0 +1,99 @@
+#include "span.hpp"
+//    io/span.cpp - Tracking info about input
+//
+//    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 "../strings/astring.hpp"
+#include "../strings/mstring.hpp"
+#include "../strings/zstring.hpp"
+
+#include "cxxstdio.hpp"
+
+#include "../poison.hpp"
+
+
+namespace tmwa
+{
+namespace io
+{
+    AString Line::message_str(ZString cat, ZString msg) const
+    {
+        MString out;
+        if (column)
+            out += STRPRINTF("%s:%u:%u: %s: %s\n"_fmt,
+                    filename, line, column, cat, msg);
+        else
+            out += STRPRINTF("%s:%u: %s: %s\n"_fmt,
+                    filename, line, cat, msg);
+        out += STRPRINTF("%s\n"_fmt, text);
+        out += STRPRINTF("%*c\n"_fmt, column, '^');
+        return AString(out);
+    }
+
+    void Line::message(ZString cat, ZString msg) const
+    {
+        if (column)
+            FPRINTF(stderr, "%s:%u:%u: %s: %s\n"_fmt,
+                    filename, line, column, cat, msg);
+        else
+            FPRINTF(stderr, "%s:%u: %s: %s\n"_fmt,
+                    filename, line, cat, msg);
+        FPRINTF(stderr, "%s\n"_fmt, text);
+        FPRINTF(stderr, "%*c\n"_fmt, column, '^');
+    }
+
+    AString LineSpan::message_str(ZString cat, ZString msg) const
+    {
+        assert (begin.column);
+        assert (end.column);
+        assert (begin.line < end.line || begin.column <= end.column);
+
+        MString out;
+        if (begin.line == end.line)
+        {
+            out += STRPRINTF("%s:%u:%u: %s: %s\n"_fmt,
+                    begin.filename, begin.line, begin.column, cat, msg);
+            out += STRPRINTF("%s\n"_fmt, begin.text);
+            out += STRPRINTF("%*c"_fmt, begin.column, '^');
+            for (unsigned c = begin.column; c != end.column; ++c)
+                out += '~';
+            out += '\n';
+        }
+        else
+        {
+            out += STRPRINTF("%s:%u:%u: %s: %s\n"_fmt,
+                    begin.filename, begin.line, begin.column, cat, msg);
+            out += STRPRINTF("%s\n"_fmt, begin.text);
+            out += STRPRINTF("%*c"_fmt, begin.column, '^');
+            for (unsigned c = begin.column; c != begin.text.size(); ++c)
+                out += '~';
+            out += " ...\n"_s;
+            out += STRPRINTF("%s\n"_fmt, end.text);
+            for (unsigned c = 0; c != end.column; ++c)
+                out += '~';
+            out += '\n';
+        }
+        return AString(out);
+    }
+
+    void LineSpan::message(ZString cat, ZString msg) const
+    {
+        FPRINTF(stderr, "%s"_fmt, message_str(cat, msg));
+    }
+} // namespace io
+} // namespace tmwa
diff --git a/src/io/span.hpp b/src/io/span.hpp
new file mode 100644
index 0000000..e474a7a
--- /dev/null
+++ b/src/io/span.hpp
@@ -0,0 +1,90 @@
+#pragma once
+//    io/span.hpp - Tracking info about input
+//
+//    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 "../strings/zstring.hpp"
+#include "../strings/literal.hpp"
+
+
+namespace tmwa
+{
+namespace io
+{
+    // TODO split this out
+    struct Line
+    {
+        RString text;
+
+        RString filename;
+        // 1-based
+        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); }
+        void error(ZString msg) const { message("error"_s, msg); }
+    };
+
+    // psst, don't tell anyone
+    struct LineChar : Line
+    {
+        char ch()
+        {
+            size_t c = column - 1;
+            if (c == text.size())
+                return '\n';
+            return text[c];
+        }
+    };
+
+    struct LineSpan
+    {
+        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;
+    };
+
+    template<class T>
+    Spanned<T> respan(LineSpan span, T data)
+    {
+        return Spanned<T>{std::move(data), std::move(span)};
+    }
+} // namespace io
+} // namespace tmwa
diff --git a/src/main-gdb-head.py b/src/main-gdb-head.py
index 6ae204b..3a05917 100644
--- a/src/main-gdb-head.py
+++ b/src/main-gdb-head.py
@@ -141,7 +141,7 @@ class PointerPrinter(object):
             try:
                 sym, off, sec, lib = info_symbol(addr)
             except:
-                s = '<heap 0x%x>' % addr
+                s = '(%s)<heap/stack 0x%x>' % (v.type, addr)
             else:
                 if off:
                     s = '<%s+%d>' % off
diff --git a/src/sexpr/lexer.hpp b/src/sexpr/lexer.hpp
index 744d8c5..9b198a0 100644
--- a/src/sexpr/lexer.hpp
+++ b/src/sexpr/lexer.hpp
@@ -58,10 +58,13 @@ namespace sexpr
         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()
+        Lexer(io::read_file_from_string, ZString name, XString str)
+        : _in(io::from_string, name, str), _current(TOK_EOF), _span(), _depth()
         { adv(); }
+        Lexer(io::read_file_from_string, ZString name, LString str)
+        : _in(io::from_string, name, str), _current(TOK_EOF), _span(), _depth()
+        { adv(); }
+
         Lexeme peek() { return _current; }
         void adv() { _current = _adv(); }
         ZString val_string() { return _string; }
diff --git a/src/sexpr/lexer_test.cpp b/src/sexpr/lexer_test.cpp
index 5904894..d84312e 100644
--- a/src/sexpr/lexer_test.cpp
+++ b/src/sexpr/lexer_test.cpp
@@ -29,22 +29,6 @@
 
 namespace tmwa
 {
-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"_s);
@@ -73,7 +57,7 @@ TEST(sexpr, escape)
 TEST(sexpr, lexer)
 {
     io::LineSpan span;
-    sexpr::Lexer lexer("<lexer-test1>"_s, string_pipe(" foo( ) 123\"\" \n"_s));
+    sexpr::Lexer lexer(io::from_string, "<lexer-test1>"_s, " foo( ) 123\"\" \n"_s);
     EXPECT_EQ(lexer.peek(), sexpr::TOK_TOKEN);
     EXPECT_EQ(lexer.val_string(), "foo"_s);
     EXPECT_EQ(lexer.span().error_str("test"_s),
@@ -120,7 +104,7 @@ TEST(sexpr, lexbad)
     QuietFd q;
     {
         io::LineSpan span;
-        sexpr::Lexer lexer("<lexer-bad>"_s, string_pipe("(\n"_s));
+        sexpr::Lexer lexer(io::from_string, "<lexer-bad>"_s, "(\n"_s);
         EXPECT_EQ(lexer.peek(), sexpr::TOK_OPEN);
         lexer.adv();
         EXPECT_EQ(lexer.peek(), sexpr::TOK_ERROR);
@@ -135,7 +119,7 @@ TEST(sexpr, lexbad)
     })
     {
         io::LineSpan span;
-        sexpr::Lexer lexer("<lexer-bad>"_s, string_pipe(bad));
+        sexpr::Lexer lexer(io::from_string, "<lexer-bad>"_s, bad);
         EXPECT_EQ(lexer.peek(), sexpr::TOK_ERROR);
     }
 }
diff --git a/src/sexpr/parser_test.cpp b/src/sexpr/parser_test.cpp
index 8619c15..bbaf5eb 100644
--- a/src/sexpr/parser_test.cpp
+++ b/src/sexpr/parser_test.cpp
@@ -27,27 +27,11 @@
 
 namespace tmwa
 {
-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>"_s, string_pipe(" foo( ) 123\"\" \n"_s));
+    sexpr::Lexer lexer(io::from_string, "<parser-test1>"_s, " foo( ) 123\"\" \n"_s);
 
     EXPECT_TRUE(sexpr::parse(lexer, s));
     EXPECT_EQ(s._type, sexpr::TOKEN);
@@ -72,7 +56,7 @@ TEST(sexpr, parser)
 TEST(sexpr, parselist)
 {
     sexpr::SExpr s;
-    sexpr::Lexer lexer("<parser-test1>"_s, string_pipe("(foo)(bar)\n"_s));
+    sexpr::Lexer lexer(io::from_string, "<parser-test1>"_s, "(foo)(bar)\n"_s);
 
     EXPECT_TRUE(sexpr::parse(lexer, s));
     EXPECT_EQ(s._type, sexpr::LIST);
@@ -108,7 +92,7 @@ TEST(sexpr, parsebad)
     {
         sexpr::SExpr s;
         io::LineSpan span;
-        sexpr::Lexer lexer("<parse-bad>"_s, string_pipe(bad));
+        sexpr::Lexer lexer(io::from_string, "<parse-bad>"_s, bad);
         EXPECT_FALSE(sexpr::parse(lexer, s));
         EXPECT_EQ(lexer.peek(), sexpr::TOK_ERROR);
     }
-- 
cgit v1.2.3-70-g09d2