From fec17ce802a4c42b3ef63fa913c82d946adc1078 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Sun, 15 Mar 2026 18:31:41 -0700 Subject: [PATCH 01/12] Add SITREP feature to produce RDF report of ACX file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lay the Archetype-side groundwork for the situation report feature: - Extract 'ROLL CALL' as a reusable method on main, called after initialization and after each turn's events - Add 'INVENTORY NAMES' message to list_inventory, which gathers inventory item names into a list without printing them - Refactor 'INVENTORY' to use 'INVENTORY NAMES' internally, so the name-gathering and display logic are separated - Add 'SITREP' method on main that assembles exits, visible objects, and inventory as a structured PairValue list of key-value pairs - SITREP is not called automatically; it is available on demand Rewrite inspect_universe for proper RDF/Turtle output The --inspect flag now produces well-formed Turtle with: - Proper prefixes: archetype:, type:, object:, attr:, msg:, xsd: - Attribute values rendered via asRDF() (e.g., attr:desc "dank cellar") instead of just listing attribute names - Type hierarchy via rdfs:Class and rdfs:subClassOf - Instance typing via rdf:type (Turtle 'a' shorthand) - Vocabulary: verb/noun phrases as archetype:verbPhrase and archetype:nounPhrase predicates on each object - Proximate objects as archetype:proximate on archetype:situation - Methods (--full flag): archetype:respondsTo msg:MESSAGE_NAME with URI-encoded message names Without --full, only object identity, types, and attribute values are emitted — this is the "state-only" view suitable for external parsers or AI systems that need to understand the game situation without knowing the internal method implementations. Co-Authored-By: Claude Opus 4.6 The SITREP must contain the same information a SystemParser uses: actual object references, not human-readable strings. A consuming program needs to know that the exit is object:south (an Archetype object it can reference), not the string "south". - Add 'INVENTORY OBJECTS' message to list_inventory, which gathers member objects (filtered by visibility) as a list of ObjectValues - Change 'SITREP' to use 'INVENTORY OBJECTS' instead of 'INVENTORY NAMES' for exits, visible objects, and inventory - Add player.location as the "location" key - Add it.referent as the "it" key when a pronoun is set The existing 'INVENTORY NAMES' and 'INVENTORY' (display) paths are unchanged. Co-Authored-By: Claude Opus 4.6 Use type:/obj: prefixes to distinguish prototypes from instances Replace per-type instance prefixes (room:, wearable:, etc.) with a single obj: prefix for all instances. The type relationship is already captured by rdf:type triples, so encoding it in the URI is redundant. This gives a clean two-prefix scheme: type: for prototypes, obj: for instances. Co-Authored-By: Claude Opus 4.6 Add TestInspectUniverse test suite Covers class/type emission for null-parent and typed instances, Turtle well-formedness of the vocabulary block (no stray leading semicolon), and the proximate list (no stray leading comma). Co-Authored-By: Claude Opus 4.6 --- games/intrptr.arch | 74 +++++++++--- src/CMakeLists.txt | 1 + src/Object.hh | 2 +- src/SystemObject.hh | 3 +- src/SystemParser.hh | 3 +- src/TestInspectUniverse.cc | 145 +++++++++++++++++++++++ src/TestInspectUniverse.hh | 28 +++++ src/Value.cc | 99 ++++++++++++++++ src/Value.hh | 13 +++ src/inspect_universe.cc | 230 ++++++++++++++++++++----------------- src/inspect_universe.hh | 2 +- src/main.cc | 12 +- src/update_universe.cc | 77 ++++++++++++- src/update_universe.hh | 5 +- 14 files changed, 561 insertions(+), 133 deletions(-) create mode 100644 src/TestInspectUniverse.cc create mode 100644 src/TestInspectUniverse.hh diff --git a/games/intrptr.arch b/games/intrptr.arch index afa573e..d228858 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 } @@ -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/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/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/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/Value.cc b/src/Value.cc index fe9b250..e66aaf2 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); @@ -437,6 +517,25 @@ namespace archetype { } } + 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 { out << PAIR << head_ << tail_; } 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 From da34c7adf185c13bffc691aa94bbe49792b69c6f Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 17 Apr 2026 21:44:05 -0700 Subject: [PATCH 02/12] Switch list literals to [] and clean up REPL result display Brackets: list literals now use [...]; {...} is exclusively a compound statement. Parser, PairValue::display, intrptr.arch sources, and tests updated. See list_design.md for the rationale (spreadsheet-model preservation, ending the list/block homoglyph). REPL: results now print as `=> value` using display() only. Dropped the trailing stringConversion tail, which duplicated scalar output (`8 8`) for values whose display and string forms happen to match. Matches Python/Ruby/Lisp convention. Co-Authored-By: Claude Opus 4.7 --- games/intrptr.arch | 12 ++++----- src/Expression.cc | 6 ++--- src/ReadEvalPrintLoop.cc | 7 +----- src/TestExpression.cc | 53 ++++++++++++++++++++++++++++++++++++++++ src/TestExpression.hh | 2 ++ src/TestValue.cc | 4 +-- src/Value.cc | 4 +-- 7 files changed, 69 insertions(+), 19 deletions(-) diff --git a/games/intrptr.arch b/games/intrptr.arch index d228858..2a6cce4 100644 --- a/games/intrptr.arch +++ b/games/intrptr.arch @@ -202,12 +202,12 @@ methods '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 + 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 := [ "it" it.referent ] @ sitrep sitrep } @@ -373,7 +373,7 @@ end class event_handler based on null - subscribers : { } + subscribers : [ ] temp : UNDEFINED event : 'EVENT' 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/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/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/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 e66aaf2..cdec34f 100644 --- a/src/Value.cc +++ b/src/Value.cc @@ -498,7 +498,7 @@ namespace archetype { tail_->display(out); out << ')'; } else { - out << '{'; + out << '['; head_->display(out); while (tail_p) { out << ' '; @@ -513,7 +513,7 @@ namespace archetype { break; } } - out << '}'; + out << ']'; } } From 3ae4dbf91170390d9ca30d75baefcaa940a896f8 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Sat, 18 Apr 2026 18:11:13 -0700 Subject: [PATCH 03/12] Address PR #19 feedback: escape vocab literals, clarify list_inventory Emit archetype:verbPhrase / nounPhrase values through a local escape_literal helper so quotes and backslashes in phrases stay valid Turtle. The helper mirrors escape_string in Value.cc, kept local to avoid a cross-TU dependency. In intrptr.arch, rename the local variable inside 'INVENTORY OBJECTS' from 'names' to 'items' to reflect that it collects objects, not strings, and declare 'items' on list_inventory alongside the existing 'names' attribute used by the sibling methods. --- games/intrptr.arch | 7 ++++--- src/inspect_universe.cc | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/games/intrptr.arch b/games/intrptr.arch index 2a6cce4..8b4ccac 100644 --- a/games/intrptr.arch +++ b/games/intrptr.arch @@ -488,18 +488,19 @@ null list_inventory temp : UNDEFINED last : UNDEFINED names : UNDEFINED + items : UNDEFINED methods 'INVENTORY OBJECTS' : { - names := UNDEFINED + items := UNDEFINED temp := sender.members while temp do { if 'INVENTORY NAME' -> head temp then - names := (head temp) @ names + items := (head temp) @ items temp := tail temp } - names + items } 'INVENTORY NAMES' : { diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index c3a5169..4c21b45 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -14,6 +14,26 @@ namespace archetype { + // Escape a string for safe embedding in a quoted Turtle literal. + // Mirrors escape_string in Value.cc; kept local to avoid a cross-TU dep. + static std::string escape_literal(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; + } + // URI-encode a message name following RFC 3986 section 2.3. static std::string uri_encode(const std::string& s) { std::ostringstream encoded; @@ -150,7 +170,7 @@ namespace archetype { if (vi != verb_objects.end()) { for (const auto& vp : vi->second) { out << "\n " << (first ? "" : "; ") - << "archetype:verbPhrase \"" << vp << "\""; + << "archetype:verbPhrase " << escape_literal(vp); first = false; } } @@ -158,7 +178,7 @@ namespace archetype { if (ni != noun_objects.end()) { for (const auto& np : ni->second) { out << "\n " << (first ? "" : "; ") - << "archetype:nounPhrase \"" << np << "\""; + << "archetype:nounPhrase " << escape_literal(np); first = false; } } From d4029045b9f28094cbc6dec9db7e6f6ec6577370 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Sat, 18 Apr 2026 19:05:12 -0700 Subject: [PATCH 04/12] Serialize parser as archetype:SystemParser; add --update --inspect The parser-context portion of --sitrep was emitting a custom text format (PROXIMATE/NOUNS/VERBS s-expressions) out of step with the Turtle produced everywhere else. Unify on Turtle and promote the parser to a first-class RDF entity. Changes in inspect_universe: - Factor the RDF emission into dump_universe_rdf (no deserialization) and write_parser_rdf, both callable independently. inspect_universe now deserializes and delegates. - Collapse archetype:verbPhrase and archetype:nounPhrase into a single archetype:matchesPhrase predicate. The kind of match (verb vs noun) is already conveyed by the object's rdf:type. - Add archetype:matchesNow, the subset of noun phrases whose referent is currently proximate. Pre-baked here so LLM-style consumers without SPARQL can answer "what can the player type right now?" with a direct scan. - Replace the standalone archetype:situation block with an archetype:parser block (a archetype:SystemParser) carrying mode, proximate, playerCommand, normalized, and parsedValue. This is the natural home for what was previously emitted as ad-hoc text. Changes in update_universe: - Delete sitrep_parser_context. --sitrep now appends proper Turtle via write_parser_rdf with prefixes. - Add an inspect parameter; --update --inspect appends the full post-turn universe (via dump_universe_rdf), closing the UX gap where there was no single invocation to get SITREP plus full game state. Tests in TestInspectUniverse updated to match the unified predicates and the relocated proximate block. --- src/Object.hh | 2 +- src/SystemObject.hh | 3 +- src/SystemParser.hh | 3 +- src/TestInspectUniverse.cc | 22 ++-- src/inspect_universe.cc | 216 ++++++++++++++++++++++--------------- src/inspect_universe.hh | 10 ++ src/main.cc | 6 +- src/update_universe.cc | 71 +++--------- src/update_universe.hh | 3 +- 9 files changed, 174 insertions(+), 162 deletions(-) diff --git a/src/Object.hh b/src/Object.hh index 0291b52..4689dc5 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, bool include_methods); + friend void dump_universe_rdf(std::ostream& out, bool include_methods); public: static const int INVALID = -1; diff --git a/src/SystemObject.hh b/src/SystemObject.hh index ac9df57..57a890e 100644 --- a/src/SystemObject.hh +++ b/src/SystemObject.hh @@ -55,8 +55,7 @@ namespace archetype { bool figureState_(const Value& message); void resetSystem_(); - friend void inspect_universe(Storage& in, std::ostream& out, bool include_methods); - friend std::string sitrep_parser_context(); + friend void write_parser_rdf(std::ostream& out, bool with_prefixes); }; } diff --git a/src/SystemParser.hh b/src/SystemParser.hh index 07fc518..3b895fb 100644 --- a/src/SystemParser.hh +++ b/src/SystemParser.hh @@ -80,8 +80,7 @@ namespace archetype { void matchVerbs_(std::list& wordValues); void matchNouns_(std::list& wordValues); - friend void inspect_universe(Storage& in, std::ostream& out, bool include_methods); - friend std::string sitrep_parser_context(); + friend void write_parser_rdf(std::ostream& out, bool with_prefixes); }; Storage& operator<<(Storage& out, const SystemParser& p); diff --git a/src/TestInspectUniverse.cc b/src/TestInspectUniverse.cc index d533813..217b141 100644 --- a/src/TestInspectUniverse.cc +++ b/src/TestInspectUniverse.cc @@ -111,10 +111,14 @@ namespace archetype { 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); + // Per-object phrases are emitted under the unified predicate. + ARCHETYPE_TEST(ttl.find("archetype:matchesPhrase \"thing\"") != string::npos); + ARCHETYPE_TEST(ttl.find("archetype:matchesPhrase \"thingamajig\"") != string::npos); + ARCHETYPE_TEST(ttl.find("archetype:matchesPhrase \"gizmo\"") != string::npos); + + // ANNOUNCE made both objects proximate, so their phrases are live too. + ARCHETYPE_TEST(ttl.find("archetype:matchesNow \"thing\"") != string::npos); + ARCHETYPE_TEST(ttl.find("archetype:matchesNow \"gizmo\"") != string::npos); } void TestInspectUniverse::testProximateSyntax_() { @@ -130,11 +134,11 @@ namespace archetype { 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); + // Proximate now lives on archetype:parser, not archetype:situation. + ARCHETYPE_TEST(ttl.find("archetype:parser a archetype:SystemParser") != string::npos); + // First object in the list must not be preceded by a comma. + ARCHETYPE_TEST(ttl.find("archetype:proximate ,") == string::npos); + ARCHETYPE_TEST(ttl.find("archetype:proximate obj:") != string::npos); } void TestInspectUniverse::runTests_() { diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index 4c21b45..68bc8bc 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -50,28 +49,43 @@ namespace archetype { 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; + static std::map build_reverse_ids() { + std::map m; for (auto const& a : Universe::instance().ObjectIdentifiers) { - reverse_ids.insert(std::make_pair(a.second, a.first)); + m.insert(std::make_pair(a.second, a.first)); + } + return m; + } + + static std::string obj_name_for(int obj_id, const std::map& reverse_ids) { + 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); + } - 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); - }; + // Join a phrase word list into a single space-separated string. + static std::string join_phrase(const std::list& words) { + std::ostringstream ss; + for (auto ii = words.begin(); ii != words.end(); ++ii) { + if (ii != words.begin()) ss << ' '; + ss << (*ii)->getString(); + } + return ss.str(); + } - // -- Prefixes -- + static std::string parser_mode_name(SystemParser::Mode_e mode) { + switch (mode) { + case SystemParser::VERBS: return "verbs"; + case SystemParser::NOUNS: return "nouns"; + } + return "unknown"; + } + static void write_rdf_prefixes(std::ostream& out) { out << "@base .\n\n" << "@prefix rdf: .\n" << "@prefix rdfs: .\n" @@ -81,8 +95,98 @@ namespace archetype { << "@prefix obj: .\n" << "@prefix attr: .\n" << "@prefix msg: .\n\n"; + } + + void write_parser_rdf(std::ostream& out, bool with_prefixes) { + ObjectPtr systemObject = Universe::instance().getObject(Universe::SystemObjectId); + SystemObject* system = dynamic_cast(systemObject.get()); + if (not system or not system->parser_) return; + const auto& parser = *system->parser_; + + if (with_prefixes) write_rdf_prefixes(out); + + std::map reverse_ids = build_reverse_ids(); + auto obj_name = [&](int id) { return obj_name_for(id, reverse_ids); }; - // -- Objects -- + // -- Vocabulary: invert match tables so phrases are grouped by object -- + + std::map> phrases_by_object; + for (const auto& vm : parser.verbMatches_) { + phrases_by_object[vm.second].insert(join_phrase(vm.first)); + } + for (const auto& nm : parser.nounMatches_) { + phrases_by_object[nm.second].insert(join_phrase(nm.first)); + } + + // "Effective" phrases: noun phrases whose referent is currently in + // scope (proximate). Pre-baked here so consumers without SPARQL can + // answer "what can the player type right now?" with a simple scan. + std::map> live_phrases_by_object; + for (const auto& nm : parser.nounMatches_) { + if (parser.proximate_.count(nm.second)) { + live_phrases_by_object[nm.second].insert(join_phrase(nm.first)); + } + } + + out << "# Vocabulary\n\n"; + for (const auto& entry : phrases_by_object) { + int obj_id = entry.first; + out << obj_name(obj_id); + bool first = true; + for (const auto& p : entry.second) { + out << "\n " << (first ? "" : "; ") + << "archetype:matchesPhrase " << escape_literal(p); + first = false; + } + auto live = live_phrases_by_object.find(obj_id); + if (live != live_phrases_by_object.end()) { + for (const auto& p : live->second) { + out << "\n ; archetype:matchesNow " << escape_literal(p); + } + } + out << " .\n\n"; + } + + // -- Parser state -- + + out << "# Parser state\n\n" + << "archetype:parser a archetype:SystemParser" + << "\n ; archetype:mode " << escape_literal(parser_mode_name(parser.mode_)); + + if (not parser.proximate_.empty()) { + bool first = true; + out << "\n ; archetype:proximate "; + for (int p_obj_id : parser.proximate_) { + if (not first) out << ", "; + out << obj_name(p_obj_id); + first = false; + } + } + + if (not parser.playerCommand_.empty()) { + out << "\n ; archetype:playerCommand " << escape_literal(parser.playerCommand_); + } + if (not parser.normalized_.empty()) { + out << "\n ; archetype:normalized " << escape_literal(parser.normalized_); + } + if (not parser.parsedValues_.empty()) { + bool first = true; + out << "\n ; archetype:parsedValue "; + for (const auto& v : parser.parsedValues_) { + if (not first) out << ", "; + out << v->asRDF(); + first = false; + } + } + + out << " .\n\n"; + } + + void dump_universe_rdf(std::ostream& out, bool include_methods) { + std::map reverse_ids = build_reverse_ids(); + auto obj_name = [&](int id) { return obj_name_for(id, reverse_ids); }; + + write_rdf_prefixes(out); for (int obj_id = 0; obj_id < Universe::instance().objectCount(); obj_id++) { ObjectPtr obj = Universe::instance().getObject(obj_id); @@ -90,7 +194,6 @@ namespace archetype { out << obj_name(obj_id); - // Type / inheritance ObjectPtr parent = obj->parent(); if (parent) { if (obj->isPrototype()) { @@ -107,7 +210,6 @@ namespace archetype { out << " a " << obj_name(Universe::NullObjectId); } - // Attributes: flat view with evaluated values for (auto const& attr : obj->attributes_) { Value value = attr.second->evaluate(); if (value->isDefined()) { @@ -116,7 +218,6 @@ namespace archetype { } } - // Methods: list which messages this object responds to if (include_methods) { for (auto const& method : obj->methods_) { if (method.first == DefaultMethod) { @@ -129,72 +230,13 @@ namespace archetype { } out << " .\n\n"; - } // objects - - // -- Vocabulary: parser state -- - - ObjectPtr systemObject = Universe::instance().getObject(Universe::SystemObjectId); - 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; - for (auto ii = phr.begin(); ii != phr.end(); ++ii) { - if (ii != phr.begin()) ss << ' '; - ss << (*ii)->getString(); - } - return ss.str(); - }; - - // Invert the parser's match tables: group phrases by object. - std::map> verb_objects; - for (const auto& vp : system->parser_->verbMatches_) { - verb_objects[vp.second].insert(phrase(vp.first)); } - std::map> noun_objects; - for (const auto& np : system->parser_->nounMatches_) { - noun_objects[np.second].insert(phrase(np.first)); - } - - // 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 " << escape_literal(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 " << escape_literal(np); - first = false; - } - } - out << " .\n\n"; - } + write_parser_rdf(out, /* with_prefixes = */ false); + } - // 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; - } - out << " .\n\n"; - } - } // inspect_universe + void inspect_universe(Storage& in, std::ostream& out, bool include_methods) { + in >> Universe::instance(); + dump_universe_rdf(out, include_methods); + } } diff --git a/src/inspect_universe.hh b/src/inspect_universe.hh index a713e5f..a866420 100644 --- a/src/inspect_universe.hh +++ b/src/inspect_universe.hh @@ -14,8 +14,18 @@ namespace archetype { + // Deserialize `in` into the Universe, then dump its full RDF/Turtle. void inspect_universe(Storage& in, std::ostream& out, bool include_methods = false); + // Dump the current Universe as RDF/Turtle (no deserialization step). + void dump_universe_rdf(std::ostream& out, bool include_methods = false); + + // Emit the parser's current state as a Turtle block describing + // archetype:parser, an instance of archetype:SystemParser. If + // `with_prefixes` is true, emits the @base/@prefix preamble first so the + // output is a self-contained Turtle fragment. + void write_parser_rdf(std::ostream& out, bool with_prefixes = false); + } #endif // __archetype__inspect_universe__ diff --git a/src/main.cc b/src/main.cc index 9706356..565e86d 100644 --- a/src/main.cc +++ b/src/main.cc @@ -91,6 +91,7 @@ void usage() { << " --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 << " --sitrep In combination with --update, append a situation report (RDF/Turtle)." << endl + << " --inspect In combination with --update, append the full post-turn universe (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 ; @@ -236,8 +237,11 @@ int main(int argc, const char* argv[]) { copy(istreambuf_iterator{f_in}, {}, back_inserter(in_mem.bytes())); } bool sitrep = opts.count("sitrep") > 0; + // --inspect with an empty value pairs with --update; a non-empty value + // selects the standalone --inspect=file.acx path handled below. + bool inspect_after = opts.count("inspect") > 0 and opts["inspect"].empty(); MemoryStorage out_mem; - cout << update_universe(in_mem, out_mem, opts["input"], width, sitrep); + cout << update_universe(in_mem, out_mem, opts["input"], width, sitrep, inspect_after); ofstream f_out(filename.c_str()); if (!f_out) { throw invalid_argument("Cannot write to " + filename); diff --git a/src/update_universe.cc b/src/update_universe.cc index 19cef6e..c085cf7 100644 --- a/src/update_universe.cc +++ b/src/update_universe.cc @@ -8,6 +8,7 @@ #include "StringOutput.hh" #include "SystemObject.hh" #include "Value.hh" +#include "inspect_universe.hh" #include @@ -63,64 +64,8 @@ Value dispatch_to_universe(string message) { return result; } -// 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) { + bool sitrep, bool inspect) { // Paging, no; wrapping, yes. UserOutput str_output{new StringOutput}; UserOutput wrapped{new WrappedOutput{str_output, width}}; @@ -138,13 +83,21 @@ string update_universe(Storage& in, Storage& out, string input, int width, string result = dynamic_cast(str_output.get())->getOutput(); if (sitrep and not Universe::instance().ended()) { + ostringstream rdf_out; try { Value sitrep_val = dispatch_to_universe("SITREP"); - result += "SITREP " + sitrep_val->asRDF() + "\n"; + rdf_out << "SITREP " << sitrep_val->asRDF() << "\n"; } catch (const std::exception&) { // SITREP not available; silently skip } - result += sitrep_parser_context(); + write_parser_rdf(rdf_out, /* with_prefixes = */ true); + result += rdf_out.str(); + } + + if (inspect and not Universe::instance().ended()) { + ostringstream rdf_out; + dump_universe_rdf(rdf_out); + result += rdf_out.str(); } out << Universe::instance(); diff --git a/src/update_universe.hh b/src/update_universe.hh index 13ea81c..00ee91a 100644 --- a/src/update_universe.hh +++ b/src/update_universe.hh @@ -17,7 +17,8 @@ namespace archetype { Value dispatch_to_universe(std::string message); std::string update_universe(Storage& in, Storage& out, std::string input, - int width = 0, bool sitrep = false); + int width = 0, bool sitrep = false, + bool inspect = false); } From 9ae925456a2c2aad758767a2279b5bb79194a691 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Sun, 19 Apr 2026 09:07:17 -0700 Subject: [PATCH 05/12] Test parser RDF block and --update --sitrep/--inspect flags - TestInspectUniverse::testParserBlock_ drives the parser through a PLAYER CMD so playerCommand_ and normalized_ are populated, then pins archetype:parser a archetype:SystemParser along with mode, playerCommand, and normalized in the Turtle output. - New TestUpdateUniverse suite exercises update_universe() across its three modes: plain (no RDF appended), --sitrep (prefixes + parser block appended), --inspect (full universe dump appended). Uses a minimal null-parent main object so the assertions don't depend on standard.arch vocabulary. --- src/CMakeLists.txt | 1 + src/TestInspectUniverse.cc | 28 +++++++++++ src/TestInspectUniverse.hh | 1 + src/TestUpdateUniverse.cc | 96 ++++++++++++++++++++++++++++++++++++++ src/TestUpdateUniverse.hh | 28 +++++++++++ 5 files changed, 154 insertions(+) create mode 100644 src/TestUpdateUniverse.cc create mode 100644 src/TestUpdateUniverse.hh diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f4f33e3..76f022b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,7 @@ TestSystemParser.cc TestSystemSorter.cc TestTokenStream.cc TestUniverse.cc +TestUpdateUniverse.cc TestValue.cc TestWrappedOutput.cc Token.cc diff --git a/src/TestInspectUniverse.cc b/src/TestInspectUniverse.cc index 217b141..037d772 100644 --- a/src/TestInspectUniverse.cc +++ b/src/TestInspectUniverse.cc @@ -141,9 +141,37 @@ namespace archetype { ARCHETYPE_TEST(ttl.find("archetype:proximate obj:") != string::npos); } + void TestInspectUniverse::testParserBlock_() { + Universe::destroy(); + + TokenStream t(make_source_from_str("inspect_test", program)); + ARCHETYPE_TEST(Universe::instance().make(t)); + + // Run setup, then hand the parser a command so playerCommand_ and + // normalized_ are populated before we inspect. + Capture capture; + Statement setup = make_stmt_from_str( + "{'go' -> setup; 'PLAYER CMD' -> system; \"look at gizmo\" -> system}"); + setup->execute(); + + string ttl = getTurtleOutput_(); + + // The parser is serialized as a first-class archetype:SystemParser. + ARCHETYPE_TEST(ttl.find("archetype:parser a archetype:SystemParser") != string::npos); + + // CLOSE PARSER leaves the parser in NOUNS mode. + ARCHETYPE_TEST(ttl.find("archetype:mode \"nouns\"") != string::npos); + + // The raw command round-trips verbatim; the normalized form is + // present (exact spacing is an implementation detail of parse()). + ARCHETYPE_TEST(ttl.find("archetype:playerCommand \"look at gizmo\"") != string::npos); + ARCHETYPE_TEST(ttl.find("archetype:normalized ") != string::npos); + } + void TestInspectUniverse::runTests_() { testNullParentType_(); testVocabSyntax_(); testProximateSyntax_(); + testParserBlock_(); } } diff --git a/src/TestInspectUniverse.hh b/src/TestInspectUniverse.hh index f7c5b2c..cbdeb06 100644 --- a/src/TestInspectUniverse.hh +++ b/src/TestInspectUniverse.hh @@ -18,6 +18,7 @@ namespace archetype { void testNullParentType_(); void testVocabSyntax_(); void testProximateSyntax_(); + void testParserBlock_(); protected: virtual void runTests_() override; public: diff --git a/src/TestUpdateUniverse.cc b/src/TestUpdateUniverse.cc new file mode 100644 index 0000000..9e16c49 --- /dev/null +++ b/src/TestUpdateUniverse.cc @@ -0,0 +1,96 @@ +// +// TestUpdateUniverse.cc +// archetype +// +// Created by Derek Jones on 2026-04-19. +// Copyright (c) 2026 Derek Jones. All rights reserved. +// + +#include + +#include "TestUpdateUniverse.hh" +#include "TestRegistry.hh" +#include "Universe.hh" +#include "SourceFile.hh" +#include "TokenStream.hh" +#include "Serialization.hh" +#include "update_universe.hh" + +using namespace std; + +namespace archetype { + ARCHETYPE_TEST_REGISTER(TestUpdateUniverse); + + // Minimal game: main responds to 'UPDATE' (required by update_universe) and + // writes a fixed string so the non-RDF portion of the output is easy to check. + static char program[] = + "null main\n" + "methods\n" + " 'UPDATE' : write \"tick\"\n" + "end\n" + ; + + static void loadProgram_(MemoryStorage& out) { + Universe::destroy(); + TokenStream t(make_source_from_str("update_test", program)); + Universe::instance().make(t); + out << Universe::instance(); + } + + void TestUpdateUniverse::testPlainUpdate_() { + MemoryStorage in_mem; + loadProgram_(in_mem); + + MemoryStorage out_mem; + string result = update_universe(in_mem, out_mem, ""); + + // Game output is returned verbatim; with neither flag set, nothing else. + ARCHETYPE_TEST(result.find("tick") != string::npos); + ARCHETYPE_TEST(result.find("@prefix") == string::npos); + ARCHETYPE_TEST(result.find("archetype:parser") == string::npos); + } + + void TestUpdateUniverse::testSitrepAppendsParserRdf_() { + MemoryStorage in_mem; + loadProgram_(in_mem); + + MemoryStorage out_mem; + string result = update_universe(in_mem, out_mem, "", 0, + /* sitrep = */ true, + /* inspect = */ false); + + // Game output comes first. + ARCHETYPE_TEST(result.find("tick") != string::npos); + + // --sitrep appends a self-contained Turtle fragment: prefix preamble + // followed by the parser block. + ARCHETYPE_TEST(result.find("@prefix archetype:") != string::npos); + ARCHETYPE_TEST(result.find("archetype:parser a archetype:SystemParser") != string::npos); + + // Without vocabulary in this tiny game, there are no matchesPhrase lines. + ARCHETYPE_TEST(result.find("archetype:matchesPhrase") == string::npos); + } + + void TestUpdateUniverse::testInspectAppendsFullRdf_() { + MemoryStorage in_mem; + loadProgram_(in_mem); + + MemoryStorage out_mem; + string result = update_universe(in_mem, out_mem, "", 0, + /* sitrep = */ false, + /* inspect = */ true); + + // Game output, then full universe dump: prefixes, objects, and the + // parser block at the end. + ARCHETYPE_TEST(result.find("tick") != string::npos); + ARCHETYPE_TEST(result.find("@prefix archetype:") != string::npos); + ARCHETYPE_TEST(result.find("obj:main a type:null") != string::npos); + ARCHETYPE_TEST(result.find("archetype:parser a archetype:SystemParser") != string::npos); + } + + void TestUpdateUniverse::runTests_() { + testPlainUpdate_(); + testSitrepAppendsParserRdf_(); + testInspectAppendsFullRdf_(); + } +} diff --git a/src/TestUpdateUniverse.hh b/src/TestUpdateUniverse.hh new file mode 100644 index 0000000..a5747ea --- /dev/null +++ b/src/TestUpdateUniverse.hh @@ -0,0 +1,28 @@ +// +// TestUpdateUniverse.hh +// archetype +// +// Created by Derek Jones on 2026-04-19. +// Copyright (c) 2026 Derek Jones. All rights reserved. +// + +#ifndef __archetype__TestUpdateUniverse__ +#define __archetype__TestUpdateUniverse__ + +#include + +#include "ITestSuite.hh" + +namespace archetype { + class TestUpdateUniverse : public ITestSuite { + void testPlainUpdate_(); + void testSitrepAppendsParserRdf_(); + void testInspectAppendsFullRdf_(); + protected: + virtual void runTests_() override; + public: + TestUpdateUniverse(std::string name): ITestSuite(name) { } + }; +} + +#endif /* defined(__archetype__TestUpdateUniverse__) */ From e2f5859b17f1de6b56030a9846382dd17b0cf0a2 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Sun, 19 Apr 2026 09:22:15 -0700 Subject: [PATCH 06/12] Emit SITREP as per-key archetype:situation predicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --sitrep path was still emitting the old ad-hoc header SITREP ( ( "location" obj:cell ) ... ) which is a Turtle collection floating without a subject/predicate — not a valid triple. Walk the list returned by 'SITREP' (a sequence of [ "key" value ] pairs) and emit each pair as its own predicate on archetype:situation, an instance of archetype:SituationReport. This gives consumers proper RDF they can query directly (?s archetype:inventory ?x) instead of traversing a nested list. Key names are sanitized to valid Turtle local-name characters; non-identifier characters fold to underscore. Trusting the game-side convention that SITREP keys are short identifier-ish words. Extends TestUpdateUniverse with testSitrepUnpacksPairs_ to pin the predicate-per-pair shape and guard against regression to the old "SITREP (" label. --- src/TestUpdateUniverse.cc | 41 +++++++++++++++++++++++++++++++++++ src/TestUpdateUniverse.hh | 1 + src/update_universe.cc | 45 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/TestUpdateUniverse.cc b/src/TestUpdateUniverse.cc index 9e16c49..96a4fe3 100644 --- a/src/TestUpdateUniverse.cc +++ b/src/TestUpdateUniverse.cc @@ -71,6 +71,46 @@ namespace archetype { ARCHETYPE_TEST(result.find("archetype:matchesPhrase") == string::npos); } + // SITREP returns a list of [key value] pairs; the emitter unpacks each + // into its own archetype: predicate on archetype:situation. + static char program_with_sitrep[] = + "null place\n" + "end\n" + "null main\n" + " s : UNDEFINED\n" + "methods\n" + " 'UPDATE' : write \"tick\"\n" + " 'SITREP' : {\n" + " s := UNDEFINED\n" + " s := [ \"location\" place ] @ s\n" + " s := [ \"mode\" \"calm\" ] @ s\n" + " s\n" + " }\n" + "end\n" + ; + + void TestUpdateUniverse::testSitrepUnpacksPairs_() { + Universe::destroy(); + TokenStream t(make_source_from_str("update_test", program_with_sitrep)); + ARCHETYPE_TEST(Universe::instance().make(t)); + MemoryStorage in_mem; + in_mem << Universe::instance(); + + MemoryStorage out_mem; + string result = update_universe(in_mem, out_mem, "", 0, + /* sitrep = */ true, + /* inspect = */ false); + + // Situation report is emitted as proper Turtle with one predicate + // per SITREP pair. + ARCHETYPE_TEST(result.find("archetype:situation a archetype:SituationReport") != string::npos); + ARCHETYPE_TEST(result.find("archetype:location obj:place") != string::npos); + ARCHETYPE_TEST(result.find("archetype:mode \"calm\"") != string::npos); + + // No vestige of the old ad-hoc "SITREP ( ... )" label. + ARCHETYPE_TEST(result.find("SITREP (") == string::npos); + } + void TestUpdateUniverse::testInspectAppendsFullRdf_() { MemoryStorage in_mem; loadProgram_(in_mem); @@ -91,6 +131,7 @@ namespace archetype { void TestUpdateUniverse::runTests_() { testPlainUpdate_(); testSitrepAppendsParserRdf_(); + testSitrepUnpacksPairs_(); testInspectAppendsFullRdf_(); } } diff --git a/src/TestUpdateUniverse.hh b/src/TestUpdateUniverse.hh index a5747ea..8520483 100644 --- a/src/TestUpdateUniverse.hh +++ b/src/TestUpdateUniverse.hh @@ -17,6 +17,7 @@ namespace archetype { class TestUpdateUniverse : public ITestSuite { void testPlainUpdate_(); void testSitrepAppendsParserRdf_(); + void testSitrepUnpacksPairs_(); void testInspectAppendsFullRdf_(); protected: virtual void runTests_() override; diff --git a/src/update_universe.cc b/src/update_universe.cc index c085cf7..7ac6104 100644 --- a/src/update_universe.cc +++ b/src/update_universe.cc @@ -47,6 +47,45 @@ namespace archetype { using namespace std; +// Sanitize a SITREP key for use as a Turtle local name. The game-side +// convention is that keys are short identifier-ish words ("location", +// "exits"), so anything outside [A-Za-z0-9_-] is replaced with '_'. Leading +// digits get an underscore prefix. An empty or all-invalid key becomes "_". +static string sanitize_local_name(const string& raw) { + if (raw.empty()) return "_"; + string out; + out.reserve(raw.size()); + for (char ch : raw) { + bool ok = (ch >= 'A' and ch <= 'Z') or (ch >= 'a' and ch <= 'z') or + (ch >= '0' and ch <= '9') or ch == '_' or ch == '-'; + out += ok ? ch : '_'; + } + if (out[0] >= '0' and out[0] <= '9') out = "_" + out; + return out; +} + +// Walk the list returned by 'SITREP' — a sequence of two-element lists of +// the form [ "key" value ] — and emit each pair as its own RDF predicate on +// archetype:situation. Example input structure: +// pair( pair("location", pair(obj:cell, undef)), +// pair( pair("exits", pair(, undef)), ... ) ) +static void write_sitrep_rdf(ostream& out, const Value& sitrep_val) { + out << "archetype:situation a archetype:SituationReport"; + Value node = sitrep_val->clone(); + while (node->isDefined()) { + Value item = node->head(); + if (not item->isDefined()) break; + Value key_val = item->head(); + Value value = item->tail()->head(); + if (key_val->isDefined() and value->isDefined()) { + string key = sanitize_local_name(key_val->getString()); + out << "\n ; archetype:" << key << " " << value->asRDF(); + } + node = node->tail(); + } + out << " .\n\n"; +} + Value dispatch_to_universe(string message) { ObjectPtr main_object = Universe::instance().getObject("main"); if (not main_object) { @@ -84,13 +123,15 @@ string update_universe(Storage& in, Storage& out, string input, int width, if (sitrep and not Universe::instance().ended()) { ostringstream rdf_out; + write_parser_rdf(rdf_out, /* with_prefixes = */ true); try { Value sitrep_val = dispatch_to_universe("SITREP"); - rdf_out << "SITREP " << sitrep_val->asRDF() << "\n"; + if (sitrep_val->isDefined()) { + write_sitrep_rdf(rdf_out, sitrep_val); + } } catch (const std::exception&) { // SITREP not available; silently skip } - write_parser_rdf(rdf_out, /* with_prefixes = */ true); result += rdf_out.str(); } From 8069e0e11a0f8f65121e8529c9bbfc4bb82e8b1e Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 15 May 2026 21:11:58 +0700 Subject: [PATCH 07/12] Skip SITREP dispatch when message is undefined Address PR #19 feedback: dispatch_to_universe("SITREP") calls Messages.index(), which inserts a new ID when the message is absent. Because update_universe serializes the universe back to disk after its work, this silently grew the message table of any saved game that lacked a SITREP handler. Guard the dispatch with Messages.find() so the table is only consulted, never mutated. Co-Authored-By: Claude Sonnet 4.6 --- src/update_universe.cc | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/update_universe.cc b/src/update_universe.cc index 7ac6104..376cc8e 100644 --- a/src/update_universe.cc +++ b/src/update_universe.cc @@ -124,13 +124,15 @@ string update_universe(Storage& in, Storage& out, string input, int width, if (sitrep and not Universe::instance().ended()) { ostringstream rdf_out; write_parser_rdf(rdf_out, /* with_prefixes = */ true); - try { - Value sitrep_val = dispatch_to_universe("SITREP"); - if (sitrep_val->isDefined()) { - write_sitrep_rdf(rdf_out, sitrep_val); + if (Universe::instance().Messages.find("SITREP") >= 0) { + try { + Value sitrep_val = dispatch_to_universe("SITREP"); + if (sitrep_val->isDefined()) { + write_sitrep_rdf(rdf_out, sitrep_val); + } + } catch (const std::exception&) { + // SITREP dispatched but failed at runtime; silently skip } - } catch (const std::exception&) { - // SITREP not available; silently skip } result += rdf_out.str(); } From ee387402e9e2ef6a3de87276686e637f23662a19 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 15 May 2026 21:12:12 +0700 Subject: [PATCH 08/12] Restrict --inspect attribute output to materialized values Address PR #19 feedback: the previous code called evaluate() on every attribute expression without setting self, which could yield wrong values for self-referential expressions and could trigger side effects for non-literal attribute defaults. Limit emission to attributes whose expressions are ValueExpression (the form Object::setAttribute stores into) and wrap the evaluation in a ContextScope, matching the pattern used in AttributeValue::dereference_(). Co-Authored-By: Claude Sonnet 4.6 --- src/inspect_universe.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index 68bc8bc..b683fb1 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -211,7 +211,11 @@ namespace archetype { } for (auto const& attr : obj->attributes_) { - Value value = attr.second->evaluate(); + auto* val_expr = dynamic_cast(attr.second.get()); + if (not val_expr) continue; + ContextScope c; + c->selfObject = obj; + Value value = val_expr->evaluate(); if (value->isDefined()) { out << "\n ; attr:" << Universe::instance().Identifiers.get(attr.first) << " " << value->asRDF(); From ba3112d93fee22ed6c648159ab597eac5e2158e2 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 15 May 2026 21:12:21 +0700 Subject: [PATCH 09/12] Document --create flag for compiling games to .acx Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index e953afc..a09194e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,12 @@ cmake --build build Always run the test suite and verify it passes before committing changes to C++ source. +## Compile a game to binary + +```shell +./build/archetype --source=games/gorreven.arch --include=games --create=games/gorreven.acx +``` + ## Smoke-test a game ```shell From 395030a846a6d6be03a90be9d99c993f80288e81 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 15 May 2026 21:12:47 +0700 Subject: [PATCH 10/12] Detect unknown command-line options Previously the option parser stuffed any --name[=value] token into the opts map and the rest of main() only count()ed the names it knew about. A misspelled flag (e.g. --compile in place of --create) was silently accepted, causing the interpreter to run in a different mode than the user intended. Refactor each consumer to find() and erase-by-iterator (single lookup, O(1) amortized erase), then validate opts and args at every exit path. Anything left over now produces a diagnostic and exit code 1. Co-Authored-By: Claude Sonnet 4.6 --- src/main.cc | 95 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/src/main.cc b/src/main.cc index 565e86d..2e2c6ef 100644 --- a/src/main.cc +++ b/src/main.cc @@ -98,13 +98,17 @@ void usage() { } static void from_source(map &opts) { - string source_path = opts["source"]; + auto it_source = opts.find("source"); + string source_path = it_source->second; + opts.erase(it_source); SourceFilePtr source = Wellspring::instance().primarySource(source_path); if (not source) { throw invalid_argument("Cannot open \"" + source_path + "\""); } - if (opts.count("include")) { - string includes = opts["include"]; + auto it_include = opts.find("include"); + if (it_include != opts.end()) { + string includes = it_include->second; + opts.erase(it_include); istringstream in(includes); string path; while (getline(in, path, ':')) { SHOW(path); @@ -116,10 +120,12 @@ static void from_source(map &opts) { throw CompilationFailure(); } Universe::instance().reportUndefinedIdentifiers(); - if (not opts.count("create")) { + auto it_create = opts.find("create"); + if (it_create == opts.end()) { dispatch_to_universe("START"); } else { - string filename_out = opts["create"]; + string filename_out = it_create->second; + opts.erase(it_create); if (filename_out.empty()) { auto iext = source_path.rfind('.'); filename_out = source_path.substr(0, iext); @@ -163,21 +169,40 @@ int main(int argc, const char* argv[]) { usage(); return 0; } - if (opts.count("help")) { + auto unknown_options_error = [&]() -> int { + if (opts.empty() and args.empty()) return 0; + cerr << "ERROR: unknown options or arguments:"; + for (auto const& kv : opts) cerr << " --" << kv.first; + for (auto const& a : args) cerr << " " << a; + cerr << endl; + return 1; + }; + auto it_help = opts.find("help"); + if (it_help != opts.end()) { + opts.erase(it_help); + if (int e = unknown_options_error()) return e; usage(); return 0; } - if (opts.count("silent")) { + auto it_silent = opts.find("silent"); + if (it_silent != opts.end()) { session.silent(true); + opts.erase(it_silent); } - if (opts.count("test")) { + auto it_test = opts.find("test"); + if (it_test != opts.end()) { + opts.erase(it_test); + if (int e = unknown_options_error()) return e; bool success = TestRegistry::instance().runAllTestSuites(cout); int exit_code = success ? 0 : 1; return exit_code; } - if (opts.count("repl")) { + auto it_repl = opts.find("repl"); + if (it_repl != opts.end()) { + opts.erase(it_repl); if (!args.empty()) { std::string filename = args.front(); + args.pop_front(); cout << "Loading " << filename << endl; InFileStorage in(filename); if (!in.ok()) { @@ -185,14 +210,16 @@ int main(int argc, const char* argv[]) { } in >> Universe::instance(); } + if (int e = unknown_options_error()) return e; int errors = repl(); return errors; } - if (opts.count("source")) { + if (opts.find("source") != opts.end()) { try { from_source(opts); } catch (const archetype::QuitGame&) { + if (int e = unknown_options_error()) return e; return 0; } catch (const std::exception& e) { cerr << "ERROR: " << e.what() << endl; @@ -200,8 +227,12 @@ int main(int argc, const char* argv[]) { } } - if (opts.count("perform")) { - string filename = opts["perform"]; + auto it_perform = opts.find("perform"); + auto it_update = opts.find("update"); + auto it_inspect = opts.find("inspect"); + if (it_perform != opts.end()) { + string filename = it_perform->second; + opts.erase(it_perform); if (filename.rfind('.') == string::npos) { filename += ".acx"; } @@ -213,19 +244,23 @@ int main(int argc, const char* argv[]) { in >> Universe::instance(); dispatch_to_universe("START"); } catch (const archetype::QuitGame&) { + if (int e = unknown_options_error()) return e; return 0; } catch (const std::exception& e) { cerr << "ERROR: " << e.what() << endl; return 1; } - } else if (opts.count("update")) { - string filename = opts["update"]; + } else if (it_update != opts.end()) { + string filename = it_update->second; + opts.erase(it_update); if (filename.rfind('.') == string::npos) { filename += ".acx"; } int width = 80; - if (opts.count("width")) { - width = stoi(opts["width"]); + auto it_width = opts.find("width"); + if (it_width != opts.end()) { + width = stoi(it_width->second); + opts.erase(it_width); } try { MemoryStorage in_mem; @@ -236,12 +271,24 @@ int main(int argc, const char* argv[]) { } copy(istreambuf_iterator{f_in}, {}, back_inserter(in_mem.bytes())); } - bool sitrep = opts.count("sitrep") > 0; + auto it_sitrep = opts.find("sitrep"); + bool sitrep = (it_sitrep != opts.end()); + if (sitrep) opts.erase(it_sitrep); // --inspect with an empty value pairs with --update; a non-empty value // selects the standalone --inspect=file.acx path handled below. - bool inspect_after = opts.count("inspect") > 0 and opts["inspect"].empty(); + bool inspect_after = false; + if (it_inspect != opts.end()) { + inspect_after = it_inspect->second.empty(); + opts.erase(it_inspect); + } + string input; + auto it_input = opts.find("input"); + if (it_input != opts.end()) { + input = it_input->second; + opts.erase(it_input); + } MemoryStorage out_mem; - cout << update_universe(in_mem, out_mem, opts["input"], width, sitrep, inspect_after); + cout << update_universe(in_mem, out_mem, input, width, sitrep, inspect_after); ofstream f_out(filename.c_str()); if (!f_out) { throw invalid_argument("Cannot write to " + filename); @@ -251,8 +298,9 @@ int main(int argc, const char* argv[]) { cerr << "ERROR: " << e.what() << endl; return 1; } - } else if (opts.count("inspect")) { - string filename = opts["inspect"]; + } else if (it_inspect != opts.end()) { + string filename = it_inspect->second; + opts.erase(it_inspect); if (filename.rfind('.') == string::npos) { filename += ".acx"; } @@ -261,12 +309,15 @@ int main(int argc, const char* argv[]) { if (!in.ok()) { throw runtime_error("Cannot open \"" + filename + "\""); } - bool full = opts.count("full") > 0; + auto it_full = opts.find("full"); + bool full = (it_full != opts.end()); + if (full) opts.erase(it_full); inspect_universe(in, cout, full); } catch (const std::exception& e) { cerr << "ERROR: " << e.what() << endl; return 1; } } + if (int e = unknown_options_error()) return e; return 0; } From 1083b6c2a7abe7249cc670807b336e12d0196aac Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 15 May 2026 21:31:41 +0700 Subject: [PATCH 11/12] Gate parser vocabulary and proximity behind --full Address PR #19 feedback: dump_universe_rdf emitted the parser block (vocabulary, matches, proximity) unconditionally even when include_methods was false, mixing world state with parser internals. Move that block under the same flag that gates method signatures. After this change: --inspect world state only (objects, types, attribute values) --inspect --full state plus methods and parser vocabulary --update --inspect state only (post-turn snapshot) --update --sitrep parser block plus per-key situation report Update the --inspect and --full help text to match. Adjust TestInspectUniverse's helper to take an include_methods argument so the vocabulary/proximate/parser-block tests can request the fuller output. Rename TestUpdateUniverse::testInspectAppendsFullRdf_ to testInspectAppendsStateRdf_ and invert its parser-block assertion to reflect the new contract. Co-Authored-By: Claude Sonnet 4.6 --- src/TestInspectUniverse.cc | 10 +++++----- src/TestUpdateUniverse.cc | 10 +++++----- src/TestUpdateUniverse.hh | 2 +- src/inspect_universe.cc | 2 +- src/main.cc | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/TestInspectUniverse.cc b/src/TestInspectUniverse.cc index 037d772..10c89ec 100644 --- a/src/TestInspectUniverse.cc +++ b/src/TestInspectUniverse.cc @@ -61,14 +61,14 @@ namespace archetype { "end\n" ; - static string getTurtleOutput_() { + static string getTurtleOutput_(bool include_methods = false) { // Serialize the universe MemoryStorage mem; mem << Universe::instance(); // Inspect it ostringstream ttl; - inspect_universe(mem, ttl); + inspect_universe(mem, ttl, include_methods); return ttl.str(); } @@ -104,7 +104,7 @@ namespace archetype { Statement stmt = make_stmt_from_str("'go' -> setup"); stmt->execute(); - string ttl = getTurtleOutput_(); + string ttl = getTurtleOutput_(/* include_methods = */ true); // Vocabulary entries must not start with "; " — the first predicate // in a subject block must not be preceded by a semicolon. @@ -132,7 +132,7 @@ namespace archetype { Statement stmt = make_stmt_from_str("'go' -> setup"); stmt->execute(); - string ttl = getTurtleOutput_(); + string ttl = getTurtleOutput_(/* include_methods = */ true); // Proximate now lives on archetype:parser, not archetype:situation. ARCHETYPE_TEST(ttl.find("archetype:parser a archetype:SystemParser") != string::npos); @@ -154,7 +154,7 @@ namespace archetype { "{'go' -> setup; 'PLAYER CMD' -> system; \"look at gizmo\" -> system}"); setup->execute(); - string ttl = getTurtleOutput_(); + string ttl = getTurtleOutput_(/* include_methods = */ true); // The parser is serialized as a first-class archetype:SystemParser. ARCHETYPE_TEST(ttl.find("archetype:parser a archetype:SystemParser") != string::npos); diff --git a/src/TestUpdateUniverse.cc b/src/TestUpdateUniverse.cc index 96a4fe3..5486737 100644 --- a/src/TestUpdateUniverse.cc +++ b/src/TestUpdateUniverse.cc @@ -111,7 +111,7 @@ namespace archetype { ARCHETYPE_TEST(result.find("SITREP (") == string::npos); } - void TestUpdateUniverse::testInspectAppendsFullRdf_() { + void TestUpdateUniverse::testInspectAppendsStateRdf_() { MemoryStorage in_mem; loadProgram_(in_mem); @@ -120,18 +120,18 @@ namespace archetype { /* sitrep = */ false, /* inspect = */ true); - // Game output, then full universe dump: prefixes, objects, and the - // parser block at the end. + // Game output, then post-turn world state: prefixes and objects. + // Parser vocabulary/state is reserved for the --full output. ARCHETYPE_TEST(result.find("tick") != string::npos); ARCHETYPE_TEST(result.find("@prefix archetype:") != string::npos); ARCHETYPE_TEST(result.find("obj:main a type:null") != string::npos); - ARCHETYPE_TEST(result.find("archetype:parser a archetype:SystemParser") != string::npos); + ARCHETYPE_TEST(result.find("archetype:parser a archetype:SystemParser") == string::npos); } void TestUpdateUniverse::runTests_() { testPlainUpdate_(); testSitrepAppendsParserRdf_(); testSitrepUnpacksPairs_(); - testInspectAppendsFullRdf_(); + testInspectAppendsStateRdf_(); } } diff --git a/src/TestUpdateUniverse.hh b/src/TestUpdateUniverse.hh index 8520483..b30216c 100644 --- a/src/TestUpdateUniverse.hh +++ b/src/TestUpdateUniverse.hh @@ -18,7 +18,7 @@ namespace archetype { void testPlainUpdate_(); void testSitrepAppendsParserRdf_(); void testSitrepUnpacksPairs_(); - void testInspectAppendsFullRdf_(); + void testInspectAppendsStateRdf_(); protected: virtual void runTests_() override; public: diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index b683fb1..bd9655a 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -236,7 +236,7 @@ namespace archetype { out << " .\n\n"; } - write_parser_rdf(out, /* with_prefixes = */ false); + if (include_methods) write_parser_rdf(out, /* with_prefixes = */ false); } void inspect_universe(Storage& in, std::ostream& out, bool include_methods) { diff --git a/src/main.cc b/src/main.cc index 2e2c6ef..57458f5 100644 --- a/src/main.cc +++ b/src/main.cc @@ -91,9 +91,9 @@ void usage() { << " --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 << " --sitrep In combination with --update, append a situation report (RDF/Turtle)." << endl - << " --inspect In combination with --update, append the full post-turn universe (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 + << " --inspect In combination with --update, append post-turn world state (RDF/Turtle)." << endl + << " --inspect=file.acx Load a saved binary file and dump its world state as RDF/Turtle." << endl + << " --full Add method signatures and parser vocabulary to the RDF output." << endl ; } From cb3600ed03bd384b39d54b28359d279a27539418 Mon Sep 17 00:00:00 2001 From: "Derek T. Jones" Date: Fri, 15 May 2026 21:42:42 +0700 Subject: [PATCH 12/12] Maintain a reverse object-identifier map Address PR #19 feedback: ObjectValue::asRDF, ObjectValue::display, and ObjectValue::identifierConversion each performed a linear scan over ObjectIdentifiers to find the name bound to an object, making RDF emission and display O(N^2) in the number of named objects. Add a parallel std::map on Universe, kept in sync at the single write site (assignObjectIdentifier) and rebuilt after deserialization. Expose it via Universe::identifierForObject, which the three call sites now use directly. inspect_universe.cc also built its own local reverse map twice (build_reverse_ids / obj_name_for) to dodge the same problem within a single emission; drop that duplication and call the Universe API. No behavior change: every entry in ObjectIdentifiers represents a unique (identifier, object) pair under current Archetype syntax (assignObjectIdentifier is only invoked at declaration time and there is no syntactic path to bind a second name to an existing object), so the reverse map is well-defined. Co-Authored-By: Claude Sonnet 4.6 --- src/Universe.cc | 10 ++++++++++ src/Universe.hh | 6 ++++++ src/Value.cc | 25 ++++++++++--------------- src/inspect_universe.cc | 34 ++++++++++------------------------ 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/Universe.cc b/src/Universe.cc index 6df5e82..c4b5c14 100644 --- a/src/Universe.cc +++ b/src/Universe.cc @@ -200,6 +200,12 @@ namespace archetype { void Universe::assignObjectIdentifier(const ObjectPtr& object, int identifier_id) { int object_id = object->id(); ObjectIdentifiers[identifier_id] = object_id; + reverseObjectIdentifiers_[object_id] = identifier_id; + } + + int Universe::identifierForObject(int object_id) const { + auto it = reverseObjectIdentifiers_.find(object_id); + return it != reverseObjectIdentifiers_.end() ? it->second : -1; } static ObjectPtr declare_object(TokenStream& t, ObjectPtr obj) { @@ -420,7 +426,11 @@ namespace archetype { u.TextLiterals.clear(); u.Identifiers.clear(); u.ObjectIdentifiers.clear(); + u.reverseObjectIdentifiers_.clear(); in >> u.Messages >> u.TextLiterals >> u.Identifiers >> u.ObjectIdentifiers; + for (auto const& kv : u.ObjectIdentifiers) { + u.reverseObjectIdentifiers_[kv.second] = kv.first; + } u.objects_.clear(); u.createReservedObjects_(); in >> u.objects_; diff --git a/src/Universe.hh b/src/Universe.hh index 6bf6d1b..a637803 100644 --- a/src/Universe.hh +++ b/src/Universe.hh @@ -96,6 +96,10 @@ namespace archetype { void assignObjectIdentifier(const ObjectPtr& object, std::string identifier); void assignObjectIdentifier(const ObjectPtr& object, int identifier_id); + // Reverse lookup of ObjectIdentifiers. Returns the identifier_id bound to + // object_id, or -1 if the object has no associated identifier. + int identifierForObject(int object_id) const; + bool identifierIsAssignedAs(int identifier_id, int object_id) const; bool make(TokenStream& t); @@ -114,6 +118,8 @@ namespace archetype { IdentifierKindMap kinds_; + IdentifierMap reverseObjectIdentifiers_; + static Universe* instance_; Universe(); diff --git a/src/Value.cc b/src/Value.cc index cdec34f..1ce1bb4 100644 --- a/src/Value.cc +++ b/src/Value.cc @@ -39,12 +39,9 @@ namespace archetype { // 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 ""; + int identifier_id = Universe::instance().identifierForObject(object_id); + if (identifier_id < 0) return ""; + return Universe::instance().Identifiers.get(identifier_id); } @@ -356,11 +353,10 @@ namespace archetype { } void ObjectValue::display(std::ostream &out) const { - for (auto const& p : Universe::instance().ObjectIdentifiers) { - if (p.second == objectId_) { - out << Universe::instance().Identifiers.get(p.first); - return; - } + int identifier_id = Universe::instance().identifierForObject(objectId_); + if (identifier_id >= 0) { + out << Universe::instance().Identifiers.get(identifier_id); + return; } out << "= 0) { + return Value{new IdentifierValue{identifier_id}}; } return Value{new UndefinedValue}; } diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index bd9655a..281a243 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -49,22 +49,14 @@ namespace archetype { return encoded.str(); } - static std::map build_reverse_ids() { - std::map m; - for (auto const& a : Universe::instance().ObjectIdentifiers) { - m.insert(std::make_pair(a.second, a.first)); - } - return m; - } - - static std::string obj_name_for(int obj_id, const std::map& reverse_ids) { - auto it = reverse_ids.find(obj_id); - if (it == reverse_ids.end()) { + static std::string obj_name_for(int obj_id) { + int identifier_id = Universe::instance().identifierForObject(obj_id); + if (identifier_id < 0) { 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); + return prefix + Universe::instance().Identifiers.get(identifier_id); } // Join a phrase word list into a single space-separated string. @@ -105,9 +97,6 @@ namespace archetype { if (with_prefixes) write_rdf_prefixes(out); - std::map reverse_ids = build_reverse_ids(); - auto obj_name = [&](int id) { return obj_name_for(id, reverse_ids); }; - // -- Vocabulary: invert match tables so phrases are grouped by object -- std::map> phrases_by_object; @@ -131,7 +120,7 @@ namespace archetype { out << "# Vocabulary\n\n"; for (const auto& entry : phrases_by_object) { int obj_id = entry.first; - out << obj_name(obj_id); + out << obj_name_for(obj_id); bool first = true; for (const auto& p : entry.second) { out << "\n " << (first ? "" : "; ") @@ -158,7 +147,7 @@ namespace archetype { out << "\n ; archetype:proximate "; for (int p_obj_id : parser.proximate_) { if (not first) out << ", "; - out << obj_name(p_obj_id); + out << obj_name_for(p_obj_id); first = false; } } @@ -183,31 +172,28 @@ namespace archetype { } void dump_universe_rdf(std::ostream& out, bool include_methods) { - std::map reverse_ids = build_reverse_ids(); - auto obj_name = [&](int id) { return obj_name_for(id, reverse_ids); }; - write_rdf_prefixes(out); for (int obj_id = 0; obj_id < Universe::instance().objectCount(); obj_id++) { ObjectPtr obj = Universe::instance().getObject(obj_id); if (not obj) continue; - out << obj_name(obj_id); + out << obj_name_for(obj_id); ObjectPtr parent = obj->parent(); if (parent) { if (obj->isPrototype()) { out << " a rdfs:Class\n" - << " ; rdfs:subClassOf " << obj_name(parent->id()); + << " ; rdfs:subClassOf " << obj_name_for(parent->id()); } else { - out << " a " << obj_name(parent->id()); + out << " a " << obj_name_for(parent->id()); } } else if (obj_id == Universe::NullObjectId) { out << " a rdfs:Class"; } else if (obj_id == Universe::SystemObjectId) { out << " a archetype:SystemObject"; } else { - out << " a " << obj_name(Universe::NullObjectId); + out << " a " << obj_name_for(Universe::NullObjectId); } for (auto const& attr : obj->attributes_) {