diff --git a/games/intrptr.arch b/games/intrptr.arch index afa573e..2a6cce4 100644 --- a/games/intrptr.arch +++ b/games/intrptr.arch @@ -141,6 +141,8 @@ null main verbmsg : UNDEFINED + sitrep : UNDEFINED + methods 'START' : { @@ -150,6 +152,13 @@ methods } # START +'ROLL CALL' : { + 'ROLL CALL' -> system + 'ANNOUNCE MEMBERS' -> player + 'ANNOUNCE MEMBERS' -> player.location + 'ANNOUNCE SELF' -> player.location + } + 'UPDATE' : { if not started then { # The INITIAL and ASSEMBLE messages are broadcast separately because @@ -158,14 +167,11 @@ methods for each do 'ASSEMBLE' -> each 'ASSEMBLE VOCABULARY' 'ENTERED' -> player.location + 'ROLL CALL' started := TRUE } 'UPDATE' -> player - 'ROLL CALL' -> system - 'ANNOUNCE MEMBERS' -> player - 'ANNOUNCE MEMBERS' -> player.location - 'ANNOUNCE SELF' -> player.location # Prompt for input and parse writes prompt; command := read; write normal @@ -191,6 +197,18 @@ methods 'MENTION SELF' -> subj 'SEND EVENT' -> after + 'ROLL CALL' + } + +'SITREP' : { + sitrep := UNDEFINED + sitrep := [ "location" player.location ] @ sitrep + sitrep := [ "exits" ('INVENTORY OBJECTS' -> compass) ] @ sitrep + sitrep := [ "visible" ('INVENTORY OBJECTS' -> player.location) ] @ sitrep + sitrep := [ "inventory" ('INVENTORY OBJECTS' -> player) ] @ sitrep + if it then + sitrep := [ "it" it.referent ] @ sitrep + sitrep } @@ -355,7 +373,7 @@ end class event_handler based on null - subscribers : { } + subscribers : [ ] temp : UNDEFINED event : 'EVENT' @@ -453,7 +471,9 @@ methods } } - 'INVENTORY' : message -> list_inventory + 'INVENTORY' : message -> list_inventory + 'INVENTORY NAMES' : message -> list_inventory + 'INVENTORY OBJECTS' : message -> list_inventory end @@ -467,26 +487,48 @@ null list_inventory number : 0 temp : UNDEFINED last : UNDEFINED + names : UNDEFINED methods + 'INVENTORY OBJECTS' : { + names := UNDEFINED + temp := sender.members + while temp do { + if 'INVENTORY NAME' -> head temp then + names := (head temp) @ names + temp := tail temp + } + names + } + + 'INVENTORY NAMES' : { + names := UNDEFINED + temp := sender.members + while temp do { + if 'INVENTORY NAME' -> head temp then + names := ('INVENTORY NAME' -> head temp) @ names + temp := tail temp + } + names + } + 'INVENTORY' : { + 'INVENTORY NAMES' # bring 'names' up to date number := 0 - temp := sender.members + temp := names last := UNDEFINED while temp do { - if 'INVENTORY NAME' -> head temp then { # we may proceed - number +:= 1 - if not last then - writes sender.intro - else { - if number > 2 then writes "," - writes " ", last - } - last := 'INVENTORY NAME' -> head temp + number +:= 1 + if not last then + writes sender.intro + else { + if number > 2 then writes "," + writes " ", last } + last := head temp temp := tail temp } if number = 0 then @@ -496,7 +538,7 @@ methods if number > 1 then writes " and" write " ", last, "." } - + names } # INVENTORY end diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b121f91..f4f33e3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ SystemParser.cc SystemSorter.cc TestExpression.cc TestIdIndex.cc +TestInspectUniverse.cc TestObject.cc TestRegistry.cc TestSerialization.cc diff --git a/src/Expression.cc b/src/Expression.cc index cffb5d0..0ff6fbb 100644 --- a/src/Expression.cc +++ b/src/Expression.cc @@ -814,9 +814,9 @@ namespace archetype { } Expression form_list_expr(TokenStream& t) { - // Called when the list has already begun, with an opening '{'. + // Called when the list has already begun, with an opening '['. stack elements; - while (t.fetch() and t.token() != Token(Token::PUNCTUATION, '}')) { + while (t.fetch() and t.token() != Token(Token::PUNCTUATION, ']')) { if (t.token() != Token(Token::PUNCTUATION, ';')) { t.didNotConsume(); } @@ -848,7 +848,7 @@ namespace archetype { } else { return nullptr; } - case '{': + case '[': return form_list_expr(t); default: return nullptr; diff --git a/src/Object.hh b/src/Object.hh index 0be1ec9..0291b52 100644 --- a/src/Object.hh +++ b/src/Object.hh @@ -33,7 +33,7 @@ namespace archetype { std::map attributes_; std::map methods_; - friend void inspect_universe(Storage& in, std::ostream& out); + friend void inspect_universe(Storage& in, std::ostream& out, bool include_methods); public: static const int INVALID = -1; diff --git a/src/ReadEvalPrintLoop.cc b/src/ReadEvalPrintLoop.cc index 91bf47b..5402cc1 100644 --- a/src/ReadEvalPrintLoop.cc +++ b/src/ReadEvalPrintLoop.cc @@ -76,13 +76,8 @@ namespace archetype { } Value result = stmt->execute(); ostringstream sout; - sout << "["; + sout << "=> "; result->display(sout); - sout << "]"; - Value result_str = result->stringConversion(); - if (result_str->isDefined()) { - sout << " " << result_str->getString(); - } sout << endl; out->put(sout.str()); } diff --git a/src/SystemObject.hh b/src/SystemObject.hh index 68507e2..ac9df57 100644 --- a/src/SystemObject.hh +++ b/src/SystemObject.hh @@ -55,7 +55,8 @@ namespace archetype { bool figureState_(const Value& message); void resetSystem_(); - friend void inspect_universe(Storage& in, std::ostream& out); + friend void inspect_universe(Storage& in, std::ostream& out, bool include_methods); + friend std::string sitrep_parser_context(); }; } diff --git a/src/SystemParser.hh b/src/SystemParser.hh index dd4b870..07fc518 100644 --- a/src/SystemParser.hh +++ b/src/SystemParser.hh @@ -80,7 +80,8 @@ namespace archetype { void matchVerbs_(std::list& wordValues); void matchNouns_(std::list& wordValues); - friend void inspect_universe(Storage& in, std::ostream& out); + friend void inspect_universe(Storage& in, std::ostream& out, bool include_methods); + friend std::string sitrep_parser_context(); }; Storage& operator<<(Storage& out, const SystemParser& p); diff --git a/src/TestExpression.cc b/src/TestExpression.cc index 6aeb3d0..e2ac143 100644 --- a/src/TestExpression.cc +++ b/src/TestExpression.cc @@ -18,6 +18,8 @@ #include "Expression.hh" #include "Serialization.hh" #include "StringInput.hh" +#include "StringOutput.hh" +#include "ReadEvalPrintLoop.hh" #include "Universe.hh" using namespace std; @@ -327,11 +329,62 @@ namespace archetype { ARCHETYPE_TEST(expr7 != nullptr); } + void TestExpression::testListLiterals_() { + // A list literal uses square brackets and evaluates to a PairValue + // chain whose display round-trips to the same bracket form. + Expression list_expr = make_expr_from_str("[1 2 3]"); + ARCHETYPE_TEST(list_expr != nullptr); + Value list_val = list_expr->evaluate()->valueConversion(); + ostringstream list_out; + list_val->display(list_out); + ARCHETYPE_TEST_EQUAL(list_out.str(), string{"[1 2 3]"}); + + // The empty list still parses. + Expression empty_expr = make_expr_from_str("[]"); + ARCHETYPE_TEST(empty_expr != nullptr); + + // Nested list literals. + Expression nested_expr = make_expr_from_str("[[1 2] [3 4]]"); + ARCHETYPE_TEST(nested_expr != nullptr); + Value nested_val = nested_expr->evaluate()->valueConversion(); + ostringstream nested_out; + nested_val->display(nested_out); + ARCHETYPE_TEST_EQUAL(nested_out.str(), string{"[[1 2] [3 4]]"}); + + // Curly braces in expression position no longer form a list literal. + Expression curly_expr = make_expr_from_str("{1 2 3}"); + ARCHETYPE_TEST(curly_expr == nullptr); + } + + void TestExpression::testReplDisplay_() { + // Drive the REPL with a few inputs and verify each result is echoed + // once with the `=> ` prefix — the display form only, matching the + // Python/Ruby/Lisp convention. No duplicated stringConversion tail. + UserInput prior_input = Universe::instance().input(); + UserOutput prior_output = Universe::instance().output(); + UserInput repl_input{new StringInput{"3 + 5\n[1 2 3]\n\"hi\"\nexit\n"}}; + UserOutput repl_output{new StringOutput}; + Universe::instance().setInput(repl_input); + Universe::instance().setOutput(repl_output); + int errors = repl(); + ARCHETYPE_TEST_EQUAL(errors, 0); + string text = dynamic_cast(repl_output.get())->getOutput(); + ARCHETYPE_TEST(text.find("=> 8\n") != string::npos); + ARCHETYPE_TEST(text.find("=> 8 8") == string::npos); + ARCHETYPE_TEST(text.find("=> [1 2 3]\n") != string::npos); + ARCHETYPE_TEST(text.find("=> \"hi\"\n") != string::npos); + ARCHETYPE_TEST(text.find("\"hi\" hi") == string::npos); + Universe::instance().setInput(prior_input); + Universe::instance().setOutput(prior_output); + } + void TestExpression::runTests_() { testTranslation_(); testEvaluation_(); testSerialization_(); testInput_(); testVerification_(); + testListLiterals_(); + testReplDisplay_(); } } diff --git a/src/TestExpression.hh b/src/TestExpression.hh index 4db66bf..8ccf463 100644 --- a/src/TestExpression.hh +++ b/src/TestExpression.hh @@ -20,6 +20,8 @@ namespace archetype { void testSerialization_(); void testInput_(); void testVerification_(); + void testListLiterals_(); + void testReplDisplay_(); protected: virtual void runTests_() override; public: diff --git a/src/TestInspectUniverse.cc b/src/TestInspectUniverse.cc new file mode 100644 index 0000000..d533813 --- /dev/null +++ b/src/TestInspectUniverse.cc @@ -0,0 +1,145 @@ +// +// TestInspectUniverse.cc +// archetype +// +// Created by Derek Jones on 2026-03-18. +// Copyright (c) 2026 Derek Jones. All rights reserved. +// + +#include +#include + +#include "TestInspectUniverse.hh" +#include "TestRegistry.hh" +#include "Universe.hh" +#include "SourceFile.hh" +#include "TokenStream.hh" +#include "Capture.hh" +#include "inspect_universe.hh" + +using namespace std; + +namespace archetype { + ARCHETYPE_TEST_REGISTER(TestInspectUniverse); + + // A typed object (widget) and a null-parent object (thing) with an attribute. + // The setup method registers vocabulary and sets proximity. + static char program[] = + "type widget based on null\n" + " desc : \"generic widget\"\n" + "methods\n" + " 'NAME' : \"gadget|widget\" -> system\n" + " 'ANNOUNCE' : 'PRESENT' -> system\n" + "end\n" + "\n" + "widget gizmo\n" + " desc : \"a nice gizmo\"\n" + "methods\n" + " 'NAME' : \"gizmo\" -> system\n" + "end\n" + "\n" + "null thing\n" + " desc : \"just a thing\"\n" + "methods\n" + " 'NAME' : \"thing|thingamajig\" -> system\n" + " 'ANNOUNCE' : 'PRESENT' -> system\n" + "end\n" + "\n" + "null setup\n" + "methods\n" + " 'go' : {\n" + " 'INIT PARSER' -> system\n" + " 'OPEN PARSER' -> system\n" + " 'NOUN LIST' -> system\n" + " 'NAME' -> gizmo\n" + " 'NAME' -> thing\n" + " 'CLOSE PARSER' -> system\n" + " 'ROLL CALL' -> system\n" + " 'ANNOUNCE' -> gizmo\n" + " 'ANNOUNCE' -> thing\n" + " }\n" + "end\n" + ; + + static string getTurtleOutput_() { + // Serialize the universe + MemoryStorage mem; + mem << Universe::instance(); + + // Inspect it + ostringstream ttl; + inspect_universe(mem, ttl); + return ttl.str(); + } + + void TestInspectUniverse::testNullParentType_() { + Universe::destroy(); + + TokenStream t(make_source_from_str("inspect_test", program)); + ARCHETYPE_TEST(Universe::instance().make(t)); + + string ttl = getTurtleOutput_(); + + // 'thing' has a null parent, so it should get "a type:null" + ARCHETYPE_TEST(ttl.find("obj:thing a type:null") != string::npos); + + // 'gizmo' inherits from widget, so it should get "a type:widget" + ARCHETYPE_TEST(ttl.find("obj:gizmo a type:widget") != string::npos); + + // 'widget' is a prototype with a parent, so it should be "a rdfs:Class" + ARCHETYPE_TEST(ttl.find("type:widget a rdfs:Class") != string::npos); + + // 'setup' also has a null parent + ARCHETYPE_TEST(ttl.find("obj:setup a type:null") != string::npos); + } + + void TestInspectUniverse::testVocabSyntax_() { + Universe::destroy(); + + TokenStream t(make_source_from_str("inspect_test", program)); + ARCHETYPE_TEST(Universe::instance().make(t)); + + // Execute setup to register vocabulary + Capture capture; + Statement stmt = make_stmt_from_str("'go' -> setup"); + stmt->execute(); + + string ttl = getTurtleOutput_(); + + // Vocabulary entries must not start with "; " — the first predicate + // in a subject block must not be preceded by a semicolon. + ARCHETYPE_TEST(ttl.find("obj:thing\n ; ") == string::npos); + ARCHETYPE_TEST(ttl.find("obj:gizmo\n ; ") == string::npos); + + // But they should have the noun phrases + ARCHETYPE_TEST(ttl.find("archetype:nounPhrase \"thing\"") != string::npos); + ARCHETYPE_TEST(ttl.find("archetype:nounPhrase \"thingamajig\"") != string::npos); + ARCHETYPE_TEST(ttl.find("archetype:nounPhrase \"gizmo\"") != string::npos); + } + + void TestInspectUniverse::testProximateSyntax_() { + Universe::destroy(); + + TokenStream t(make_source_from_str("inspect_test", program)); + ARCHETYPE_TEST(Universe::instance().make(t)); + + // Execute setup to register vocabulary and proximity + Capture capture; + Statement stmt = make_stmt_from_str("'go' -> setup"); + stmt->execute(); + + string ttl = getTurtleOutput_(); + + // The proximate list must not have a leading comma before the first object. + // Valid: "archetype:proximate\n obj:gizmo" + // Invalid: "archetype:proximate\n , obj:gizmo" + ARCHETYPE_TEST(ttl.find("archetype:proximate\n , ") == string::npos); + ARCHETYPE_TEST(ttl.find("archetype:proximate\n obj:") != string::npos); + } + + void TestInspectUniverse::runTests_() { + testNullParentType_(); + testVocabSyntax_(); + testProximateSyntax_(); + } +} diff --git a/src/TestInspectUniverse.hh b/src/TestInspectUniverse.hh new file mode 100644 index 0000000..f7c5b2c --- /dev/null +++ b/src/TestInspectUniverse.hh @@ -0,0 +1,28 @@ +// +// TestInspectUniverse.hh +// archetype +// +// Created by Derek Jones on 2026-03-18. +// Copyright (c) 2026 Derek Jones. All rights reserved. +// + +#ifndef __archetype__TestInspectUniverse__ +#define __archetype__TestInspectUniverse__ + +#include + +#include "ITestSuite.hh" + +namespace archetype { + class TestInspectUniverse : public ITestSuite { + void testNullParentType_(); + void testVocabSyntax_(); + void testProximateSyntax_(); + protected: + virtual void runTests_() override; + public: + TestInspectUniverse(std::string name): ITestSuite(name) { } + }; +} + +#endif /* defined(__archetype__TestInspectUniverse__) */ diff --git a/src/TestValue.cc b/src/TestValue.cc index 40ac1d2..60a2fb5 100644 --- a/src/TestValue.cc +++ b/src/TestValue.cc @@ -85,11 +85,11 @@ namespace archetype { // Now create a couple of short lists Value node1{new PairValue{Value{new StringValue{"world"}}, Value{new UndefinedValue}}}; actual = display(node1); - expected = "{\"world\"}"; + expected = "[\"world\"]"; ARCHETYPE_TEST_EQUAL(actual, expected); Value node2{new PairValue{Value{new StringValue{"hello"}}, std::move(node1)}}; actual = display(node2); - expected = "{\"hello\" \"world\"}"; + expected = "[\"hello\" \"world\"]"; ARCHETYPE_TEST_EQUAL(actual, expected); } diff --git a/src/Value.cc b/src/Value.cc index fe9b250..cdec34f 100644 --- a/src/Value.cc +++ b/src/Value.cc @@ -18,6 +18,36 @@ using namespace std; namespace archetype { + // Escape a string for safe embedding in a quoted literal. + static std::string escape_string(const std::string& s) { + std::string result; + result.reserve(s.size() + 2); + result += '"'; + for (char ch : s) { + switch (ch) { + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: result += ch; break; + } + } + result += '"'; + return result; + } + + // Look up the identifier name bound to an object, or empty string if none. + static std::string identifier_of(int object_id) { + for (auto const& p : Universe::instance().ObjectIdentifiers) { + if (p.second == object_id) { + return Universe::instance().Identifiers.get(p.first); + } + } + return ""; + } + + enum ValueType_e { UNDEFINED, ABSENT, @@ -99,6 +129,10 @@ namespace archetype { out << Keywords::instance().Reserved.get(Keywords::RW_UNDEFINED); } + std::string UndefinedValue::asRDF() const { + return "archetype:UNDEFINED"; + } + void UndefinedValue::write(Storage& out) const { out << UNDEFINED; } @@ -112,6 +146,10 @@ namespace archetype { out << Keywords::instance().Reserved.get(Keywords::RW_ABSENT); } + std::string AbsentValue::asRDF() const { + return "archetype:ABSENT"; + } + void AbsentValue::write(Storage& out) const { out << ABSENT; } @@ -125,6 +163,10 @@ namespace archetype { out << Keywords::instance().Reserved.get(Keywords::RW_BREAK); } + std::string BreakValue::asRDF() const { + return "archetype:BREAK"; + } + void BreakValue::write(Storage& out) const { out << BREAK; } @@ -140,6 +182,10 @@ namespace archetype { Keywords::RW_FALSE); } + std::string BooleanValue::asRDF() const { + return value_ ? "\"true\"^^xsd:boolean" : "\"false\"^^xsd:boolean"; + } + void BooleanValue::write(Storage& out) const { out << BOOLEAN << static_cast(value_); } @@ -173,6 +219,10 @@ namespace archetype { out << "'" << Universe::instance().Messages.get(message_) << "'"; } + std::string MessageValue::asRDF() const { + return escape_string(Universe::instance().Messages.get(message_)) + "^^archetype:message"; + } + void MessageValue::write(Storage& out) const { out << MESSAGE << message_; } @@ -207,6 +257,10 @@ namespace archetype { out << '"' << Universe::instance().TextLiterals.get(textLiteral_) << '"'; } + std::string TextLiteralValue::asRDF() const { + return escape_string(getString()); + } + void TextLiteralValue::write(Storage& out) const { out << TEXT_LITERAL << textLiteral_; } @@ -220,6 +274,10 @@ namespace archetype { out << value_; } + std::string NumericValue::asRDF() const { + return std::to_string(value_); + } + void NumericValue::write(Storage& out) const { out << NUMERIC << value_; } @@ -243,6 +301,10 @@ namespace archetype { out << '"' << value_ << '"'; } + std::string StringValue::asRDF() const { + return escape_string(value_); + } + void StringValue::write(Storage& out) const { out << STRING; int text_length = static_cast(value_.size()); @@ -271,6 +333,10 @@ namespace archetype { out << Universe::instance().Identifiers.get(id_); } + std::string IdentifierValue::asRDF() const { + return "archetype:" + Universe::instance().Identifiers.get(id_); + } + int IdentifierValue::getIdentifier() const { return id_; } @@ -308,6 +374,16 @@ namespace archetype { out << '>'; } + std::string ObjectValue::asRDF() const { + std::string name = identifier_of(objectId_); + if (name.empty()) { + return "_:object_" + std::to_string(objectId_); + } + ObjectPtr obj = Universe::instance().getObject(objectId_); + std::string prefix = obj->isPrototype() ? "type:" : "obj:"; + return prefix + name; + } + void ObjectValue::write(Storage& out) const { out << OBJECT << objectId_; } @@ -330,6 +406,10 @@ namespace archetype { return other_p and other_p->objectId_ == objectId_ and other_p->attributeId_ == attributeId_; } + std::string AttributeValue::asRDF() const { + return dereference_()->asRDF(); + } + void AttributeValue::display(std::ostream &out) const { Value obj_v{new ObjectValue{objectId_}}; obj_v->display(out); @@ -418,7 +498,7 @@ namespace archetype { tail_->display(out); out << ')'; } else { - out << '{'; + out << '['; head_->display(out); while (tail_p) { out << ' '; @@ -433,8 +513,27 @@ namespace archetype { break; } } - out << '}'; + out << ']'; + } + } + + std::string PairValue::asRDF() const { + // Render as a Turtle collection: ( item1 item2 ... ) + std::string result = "( "; + result += head_->asRDF(); + const PairValue* p = dynamic_cast(tail_.get()); + while (p) { + result += " "; + result += p->head_->asRDF(); + const PairValue* next = dynamic_cast(p->tail_.get()); + if (not next and p->tail_->isDefined()) { + result += " "; + result += p->tail_->asRDF(); + } + p = next; } + result += " )"; + return result; } void PairValue::write(Storage &out) const { diff --git a/src/Value.hh b/src/Value.hh index c7c177c..f50e445 100644 --- a/src/Value.hh +++ b/src/Value.hh @@ -36,6 +36,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const = 0; virtual Value clone() const = 0; virtual void display(std::ostream& out) const = 0; + virtual std::string asRDF() const = 0; virtual void write(Storage& out) const = 0; virtual bool isTrueEnough() const { return true; } @@ -65,6 +66,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new UndefinedValue}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isDefined() const override { return false; } @@ -77,6 +79,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new AbsentValue}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isDefined() const override { return true; } @@ -89,6 +92,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new BreakValue}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isDefined() const override { return true; } @@ -102,6 +106,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new BooleanValue(value_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isTrueEnough() const override { return value_; } @@ -117,6 +122,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new MessageValue{message_}}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getMessage() const override; @@ -133,6 +139,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new TextLiteralValue{textLiteral_}}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual std::string getString() const override; @@ -150,6 +157,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new NumericValue(value_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getNumber() const override; @@ -166,6 +174,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new StringValue(value_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual std::string getString() const override; @@ -183,6 +192,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new IdentifierValue(id_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getIdentifier() const override; @@ -198,6 +208,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new ObjectValue{objectId_}}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getObject() const override; @@ -220,6 +231,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new AttributeValue(objectId_, attributeId_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getIdentifier() const override; @@ -253,6 +265,7 @@ namespace archetype { virtual Value tail() const override; virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; }; diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index 8bddcb4..c3a5169 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -3,156 +3,178 @@ #include #include #include +#include +#include #include "inspect_universe.hh" #include "Universe.hh" #include "Object.hh" +#include "Expression.hh" #include "SystemObject.hh" namespace archetype { - void inspect_universe(Storage& in, std::ostream& out) { - in >> Universe::instance(); - out << "@base .\n\n" - << "@prefix rdf: .\n" - << "@prefix rdfs: .\n" - << "@prefix owl: .\n\n" - << "@prefix system: .\n" - << "@prefix type: .\n" - << "@prefix object: .\n\n\n" - ; - std::map reverse_ids; - auto object_name = [&](int obj_id) -> std::string { - auto obj_name_iter = reverse_ids.find(obj_id); - if (obj_name_iter == reverse_ids.end()) { - return "_:object_" + std::to_string(obj_id); + // URI-encode a message name following RFC 3986 section 2.3. + static std::string uri_encode(const std::string& s) { + std::ostringstream encoded; + for (auto ch : s) { + if ((ch >= 'A' and ch <= 'Z') or (ch >= 'a' and ch <= 'z') or + (ch >= '0' and ch <= '9') or + ch == '.' or ch == '_' or ch == '-' or ch == '~') { + encoded << ch; } else { - ObjectPtr obj = Universe::instance().getObject(obj_id); - std::string prefix = obj->isPrototype() ? "type:" : "object:"; - return prefix + Universe::instance().Identifiers.get(obj_name_iter->second); + encoded << '%' << std::setw(2) << std::setfill('0') << std::hex + << (static_cast(static_cast(ch))); } - }; + } + return encoded.str(); + } + + void inspect_universe(Storage& in, std::ostream& out, bool include_methods) { + in >> Universe::instance(); + + // -- Build reverse lookup: object_id -> identifier_id -- + + std::map reverse_ids; for (auto const& a : Universe::instance().ObjectIdentifiers) { reverse_ids.insert(std::make_pair(a.second, a.first)); } + + auto obj_name = [&](int obj_id) -> std::string { + auto it = reverse_ids.find(obj_id); + if (it == reverse_ids.end()) { + return "_:object_" + std::to_string(obj_id); + } + ObjectPtr obj = Universe::instance().getObject(obj_id); + std::string prefix = obj->isPrototype() ? "type:" : "obj:"; + return prefix + Universe::instance().Identifiers.get(it->second); + }; + + // -- Prefixes -- + + out << "@base .\n\n" + << "@prefix rdf: .\n" + << "@prefix rdfs: .\n" + << "@prefix xsd: .\n\n" + << "@prefix archetype: .\n" + << "@prefix type: .\n" + << "@prefix obj: .\n" + << "@prefix attr: .\n" + << "@prefix msg: .\n\n"; + + // -- Objects -- + for (int obj_id = 0; obj_id < Universe::instance().objectCount(); obj_id++) { - out << object_name(obj_id); ObjectPtr obj = Universe::instance().getObject(obj_id); + if (not obj) continue; + + out << obj_name(obj_id); + + // Type / inheritance ObjectPtr parent = obj->parent(); if (parent) { if (obj->isPrototype()) { out << " a rdfs:Class\n" - << " ; rdfs:subClassOf "; + << " ; rdfs:subClassOf " << obj_name(parent->id()); } else { - out << " a "; + out << " a " << obj_name(parent->id()); } - auto parent_name_iter = reverse_ids.find(parent->id()); - assert(parent_name_iter != reverse_ids.end()); - out << "type:" << Universe::instance().Identifiers.get(parent_name_iter->second); - } else if (obj_id == 0) { + } else if (obj_id == Universe::NullObjectId) { out << " a rdfs:Class"; - } else if (obj_id == 1) { - out << " a system:object"; + } else if (obj_id == Universe::SystemObjectId) { + out << " a archetype:SystemObject"; + } else { + out << " a " << obj_name(Universe::NullObjectId); } - out << '\n'; - // This is the flat, not the inherited, view of the attributes and methods. - // Only what is added by each type. + + // Attributes: flat view with evaluated values for (auto const& attr : obj->attributes_) { - // Archetype has an endearing (sigh) way of instantiating any attributes that have even been - // observed with UNDEFINED. This is one reason why saved games have greater size. - const bool hide_undefined = true; - auto value = dynamic_cast(attr.second.get()); - if (!hide_undefined or (value and value->evaluate()->isDefined())) { - out << " ; prop:hasAttribute attr:" << Universe::instance().Identifiers.get(attr.first) << '\n'; + Value value = attr.second->evaluate(); + if (value->isDefined()) { + out << "\n ; attr:" << Universe::instance().Identifiers.get(attr.first) + << " " << value->asRDF(); } } - for (auto const& method : obj->methods_) { - if (method.first == DefaultMethod) { - out << " ; prop:hasMethodForMessage system:default" << '\n'; - } else { - std::string message = Universe::instance().Messages.get(method.first); - std::ostringstream encoded; - // Following RFC 3986 section 2.3 Unreserved Characters (January 2005), - // URI-encoding any characters that are not unreserved. - for (auto ch : message) { - if ((ch >= 'A' and ch <= 'Z') or (ch >= 'a' and ch <= 'z') or - ch == '.' or ch == '_' or ch == '-' or ch == '~') { - encoded << ch; - } else { - encoded << '%' << std::setw(2) << std::setfill('0') << std::hex << int(ch); - } + + // Methods: list which messages this object responds to + if (include_methods) { + for (auto const& method : obj->methods_) { + if (method.first == DefaultMethod) { + out << "\n ; archetype:respondsTo archetype:default"; + } else { + std::string message = Universe::instance().Messages.get(method.first); + out << "\n ; archetype:respondsTo msg:" << uri_encode(message); } - out << " ; prop:respondstoMessage \n"; } } - out << ".\n\n"; - } // display all objects - // Next, a situation report. What does the parser know? + out << " .\n\n"; + } // objects + + // -- Vocabulary: parser state -- + ObjectPtr systemObject = Universe::instance().getObject(Universe::SystemObjectId); - SystemObject* system_object = dynamic_cast(systemObject.get()); - assert(system_object != nullptr); - // What verb and noun phrase match what objects? - // We need to turn the parsing inside out here; the parsed matches are sorted - // from longest to shortest, not arranged by object. + SystemObject* system = dynamic_cast(systemObject.get()); + assert(system != nullptr); + // Join a phrase word list into a single quoted string. auto phrase = [](const std::list& phr) -> std::string { std::ostringstream ss; - ss << '"'; for (auto ii = phr.begin(); ii != phr.end(); ++ii) { if (ii != phr.begin()) ss << ' '; ss << (*ii)->getString(); } - ss << '"'; return ss.str(); }; + + // Invert the parser's match tables: group phrases by object. std::map> verb_objects; - std::set all_verb_phrases; - for (const auto& vp : system_object->parser_->verbMatches_) { - std::string phr = phrase(vp.first); - all_verb_phrases.insert(phr); - verb_objects[vp.second].insert(phr); + for (const auto& vp : system->parser_->verbMatches_) { + verb_objects[vp.second].insert(phrase(vp.first)); } std::map> noun_objects; - std::set all_noun_phrases; - for (const auto& np : system_object->parser_->nounMatches_) { - std::string phr = phrase(np.first); - all_noun_phrases.insert(phr); - noun_objects[np.second].insert(phr); + for (const auto& np : system->parser_->nounMatches_) { + noun_objects[np.second].insert(phrase(np.first)); } - out << "VERBS:\n"; - for (const auto& vi : verb_objects) { - out << object_name(vi.first) << " matched by "; - std::copy(vi.second.begin(), vi.second.end(), std::ostream_iterator(out, " ")); - out << '\n'; - } - out << "NOUNS:\n"; - for (const auto& ni : noun_objects) { - out << object_name(ni.first) << " matched by "; - std::copy(ni.second.begin(), ni.second.end(), std::ostream_iterator(out, " ")); - out << '\n'; - } - out << "Proximate:"; - for (int p_obj_id : system_object->parser_->proximate_) { - out << ' ' << object_name(p_obj_id); + // Emit vocabulary as RDF: each object's verb/noun phrases. + out << "# Vocabulary\n\n"; + std::set vocab_objects; + for (const auto& vi : verb_objects) vocab_objects.insert(vi.first); + for (const auto& ni : noun_objects) vocab_objects.insert(ni.first); + + for (int vo : vocab_objects) { + out << obj_name(vo); + bool first = true; + auto vi = verb_objects.find(vo); + if (vi != verb_objects.end()) { + for (const auto& vp : vi->second) { + out << "\n " << (first ? "" : "; ") + << "archetype:verbPhrase \"" << vp << "\""; + first = false; + } + } + auto ni = noun_objects.find(vo); + if (ni != noun_objects.end()) { + for (const auto& np : ni->second) { + out << "\n " << (first ? "" : "; ") + << "archetype:nounPhrase \"" << np << "\""; + first = false; + } + } + out << " .\n\n"; } - out << '\n'; - // Put out the entire vocabulary list as JSON object of two arrays. - // Crude, but we can count on it here, with a very simple structure. - // The words will already have quotes around them. - auto json_list = [](const std::set& words) -> std::string { - std::ostringstream ss; - ss << '['; - for (auto ii = words.begin(); ii != words.end(); ++ii) { - if (ii != words.begin()) ss << ", "; - ss << *ii; + // Proximate objects + if (not system->parser_->proximate_.empty()) { + out << "# Proximate objects (present in current context)\n\n" + << "archetype:situation archetype:proximate"; + bool first = true; + for (int p_obj_id : system->parser_->proximate_) { + out << "\n " << (first ? "" : ", ") << obj_name(p_obj_id); + first = false; } - ss << ']'; - return ss.str(); - }; - out << "{\"verbs\": " << json_list(all_verb_phrases) << ",\n"; - out << "\"nouns\": " << json_list(all_noun_phrases) << "}\n"; + out << " .\n\n"; + } } // inspect_universe -} \ No newline at end of file +} diff --git a/src/inspect_universe.hh b/src/inspect_universe.hh index 2089327..a713e5f 100644 --- a/src/inspect_universe.hh +++ b/src/inspect_universe.hh @@ -14,7 +14,7 @@ namespace archetype { - void inspect_universe(Storage& in, std::ostream& out); + void inspect_universe(Storage& in, std::ostream& out, bool include_methods = false); } diff --git a/src/main.cc b/src/main.cc index bebcab7..9706356 100644 --- a/src/main.cc +++ b/src/main.cc @@ -89,7 +89,10 @@ void usage() { << " --create[=file.acx] Don't run, but write the program given by --source to a binary file." << endl << " --perform=file.acx Load a saved binary file and send 'START' -> main." << endl << " --update=file.acx Load binary, send 'UPDATE' -> main, save resulting binary to the same file." << endl - << " --input In combination with --update, provide command input as a string." << endl + << " --input= In combination with --update, provide command input as a string." << endl + << " --sitrep In combination with --update, append a situation report (RDF/Turtle)." << endl + << " --inspect=file.acx Load a saved binary file and dump its contents as RDF/Turtle." << endl + << " --full Include method signatures in the RDF output." << endl ; } @@ -232,8 +235,9 @@ int main(int argc, const char* argv[]) { } copy(istreambuf_iterator{f_in}, {}, back_inserter(in_mem.bytes())); } + bool sitrep = opts.count("sitrep") > 0; MemoryStorage out_mem; - cout << update_universe(in_mem, out_mem, opts["input"], width); + cout << update_universe(in_mem, out_mem, opts["input"], width, sitrep); ofstream f_out(filename.c_str()); if (!f_out) { throw invalid_argument("Cannot write to " + filename); @@ -253,8 +257,8 @@ int main(int argc, const char* argv[]) { if (!in.ok()) { throw runtime_error("Cannot open \"" + filename + "\""); } - // TODO: no, take an output filename - inspect_universe(in, cout); + bool full = opts.count("full") > 0; + inspect_universe(in, cout, full); } catch (const std::exception& e) { cerr << "ERROR: " << e.what() << endl; return 1; diff --git a/src/update_universe.cc b/src/update_universe.cc index 43cf1a3..19cef6e 100644 --- a/src/update_universe.cc +++ b/src/update_universe.cc @@ -4,11 +4,13 @@ #include "Serialization.hh" #include "Universe.hh" #include "WrappedOutput.hh" -#include "ConsoleOutput.hh" #include "StringInput.hh" #include "StringOutput.hh" +#include "SystemObject.hh" #include "Value.hh" +#include + namespace archetype { class EchoingInput : public IUserInput { @@ -61,7 +63,64 @@ Value dispatch_to_universe(string message) { return result; } -string update_universe(Storage& in, Storage& out, string input, int width) { +// Produce the parser-level portion of the situation report: proximate objects, +// effective noun phrases (those matching proximate objects), and all verb phrases. +// Output is RDF/Turtle collections, one per line, prefixed by a label. +string sitrep_parser_context() { + ObjectPtr sys = Universe::instance().getObject(Universe::SystemObjectId); + SystemObject* system = dynamic_cast(sys.get()); + if (not system or not system->parser_) return ""; + + const auto& parser = *system->parser_; + + // Helper: join a phrase word list into a single string. + auto phrase_str = [](const list& words) -> string { + ostringstream ss; + for (auto ii = words.begin(); ii != words.end(); ++ii) { + if (ii != words.begin()) ss << ' '; + ss << (*ii)->getString(); + } + return ss.str(); + }; + + // Helper: render an object reference as an RDF URI. + auto obj_ref = [](int obj_id) -> string { + ObjectValue ov{obj_id}; + return ov.asRDF(); + }; + + ostringstream out; + + // Proximate objects + if (not parser.proximate_.empty()) { + out << "PROXIMATE ("; + for (int obj_id : parser.proximate_) { + out << " " << obj_ref(obj_id); + } + out << " )\n"; + } + + // Effective nouns: phrases matching proximate objects + out << "NOUNS ("; + for (const auto& nm : parser.nounMatches_) { + if (parser.proximate_.count(nm.second)) { + out << " ( \"" << phrase_str(nm.first) << "\" " << obj_ref(nm.second) << " )"; + } + } + out << " )\n"; + + // All verb phrases + out << "VERBS ("; + for (const auto& vm : parser.verbMatches_) { + out << " ( \"" << phrase_str(vm.first) << "\" " << obj_ref(vm.second) << " )"; + } + out << " )\n"; + + return out.str(); +} + +string update_universe(Storage& in, Storage& out, string input, int width, + bool sitrep) { // Paging, no; wrapping, yes. UserOutput str_output{new StringOutput}; UserOutput wrapped{new WrappedOutput{str_output, width}}; @@ -76,8 +135,20 @@ string update_universe(Storage& in, Storage& out, string input, int width) { } catch (const archetype::QuitGame&) { Universe::instance().endItAll(); } + string result = dynamic_cast(str_output.get())->getOutput(); + + if (sitrep and not Universe::instance().ended()) { + try { + Value sitrep_val = dispatch_to_universe("SITREP"); + result += "SITREP " + sitrep_val->asRDF() + "\n"; + } catch (const std::exception&) { + // SITREP not available; silently skip + } + result += sitrep_parser_context(); + } + out << Universe::instance(); - return dynamic_cast(str_output.get())->getOutput(); + return result; } } // namespace archetype diff --git a/src/update_universe.hh b/src/update_universe.hh index 1b5cf28..13ea81c 100644 --- a/src/update_universe.hh +++ b/src/update_universe.hh @@ -16,8 +16,9 @@ namespace archetype { Value dispatch_to_universe(std::string message); - std::string update_universe(Storage& in, Storage& out, std::string input, int width = 0); - + std::string update_universe(Storage& in, Storage& out, std::string input, + int width = 0, bool sitrep = false); + } #endif