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 diff --git a/games/intrptr.arch b/games/intrptr.arch index afa573e..8b4ccac 100644 --- a/games/intrptr.arch +++ b/games/intrptr.arch @@ -141,6 +141,8 @@ null main verbmsg : UNDEFINED + sitrep : UNDEFINED + methods 'START' : { @@ -150,6 +152,13 @@ methods } # START +'ROLL CALL' : { + 'ROLL CALL' -> system + 'ANNOUNCE MEMBERS' -> player + 'ANNOUNCE MEMBERS' -> player.location + 'ANNOUNCE SELF' -> player.location + } + 'UPDATE' : { if not started then { # The INITIAL and ASSEMBLE messages are broadcast separately because @@ -158,14 +167,11 @@ methods for each do 'ASSEMBLE' -> each 'ASSEMBLE VOCABULARY' 'ENTERED' -> player.location + 'ROLL CALL' started := TRUE } 'UPDATE' -> player - 'ROLL CALL' -> system - 'ANNOUNCE MEMBERS' -> player - 'ANNOUNCE MEMBERS' -> player.location - 'ANNOUNCE SELF' -> player.location # Prompt for input and parse writes prompt; command := read; write normal @@ -191,6 +197,18 @@ methods 'MENTION SELF' -> subj 'SEND EVENT' -> after + 'ROLL CALL' + } + +'SITREP' : { + sitrep := UNDEFINED + sitrep := [ "location" player.location ] @ sitrep + sitrep := [ "exits" ('INVENTORY OBJECTS' -> compass) ] @ sitrep + sitrep := [ "visible" ('INVENTORY OBJECTS' -> player.location) ] @ sitrep + sitrep := [ "inventory" ('INVENTORY OBJECTS' -> player) ] @ sitrep + if it then + sitrep := [ "it" it.referent ] @ sitrep + sitrep } @@ -355,7 +373,7 @@ end class event_handler based on null - subscribers : { } + subscribers : [ ] temp : UNDEFINED event : 'EVENT' @@ -453,7 +471,9 @@ methods } } - 'INVENTORY' : message -> list_inventory + 'INVENTORY' : message -> list_inventory + 'INVENTORY NAMES' : message -> list_inventory + 'INVENTORY OBJECTS' : message -> list_inventory end @@ -467,26 +487,49 @@ null list_inventory number : 0 temp : UNDEFINED last : UNDEFINED + names : UNDEFINED + items : UNDEFINED methods + 'INVENTORY OBJECTS' : { + items := UNDEFINED + temp := sender.members + while temp do { + if 'INVENTORY NAME' -> head temp then + items := (head temp) @ items + temp := tail temp + } + items + } + + '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 +539,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..76f022b 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 @@ -31,6 +32,7 @@ TestSystemParser.cc TestSystemSorter.cc TestTokenStream.cc TestUniverse.cc +TestUpdateUniverse.cc TestValue.cc TestWrappedOutput.cc Token.cc diff --git a/src/Expression.cc b/src/Expression.cc index cffb5d0..0ff6fbb 100644 --- a/src/Expression.cc +++ b/src/Expression.cc @@ -814,9 +814,9 @@ namespace archetype { } Expression form_list_expr(TokenStream& t) { - // Called when the list has already begun, with an opening '{'. + // Called when the list has already begun, with an opening '['. stack elements; - while (t.fetch() and t.token() != Token(Token::PUNCTUATION, '}')) { + while (t.fetch() and t.token() != Token(Token::PUNCTUATION, ']')) { if (t.token() != Token(Token::PUNCTUATION, ';')) { t.didNotConsume(); } @@ -848,7 +848,7 @@ namespace archetype { } else { return nullptr; } - case '{': + case '[': return form_list_expr(t); default: return nullptr; diff --git a/src/Object.hh b/src/Object.hh index 0be1ec9..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); + friend void dump_universe_rdf(std::ostream& out, bool include_methods); public: static const int INVALID = -1; diff --git a/src/ReadEvalPrintLoop.cc b/src/ReadEvalPrintLoop.cc index 91bf47b..5402cc1 100644 --- a/src/ReadEvalPrintLoop.cc +++ b/src/ReadEvalPrintLoop.cc @@ -76,13 +76,8 @@ namespace archetype { } Value result = stmt->execute(); ostringstream sout; - sout << "["; + sout << "=> "; result->display(sout); - sout << "]"; - Value result_str = result->stringConversion(); - if (result_str->isDefined()) { - sout << " " << result_str->getString(); - } sout << endl; out->put(sout.str()); } diff --git a/src/SystemObject.hh b/src/SystemObject.hh index 68507e2..57a890e 100644 --- a/src/SystemObject.hh +++ b/src/SystemObject.hh @@ -55,7 +55,7 @@ namespace archetype { bool figureState_(const Value& message); void resetSystem_(); - friend void inspect_universe(Storage& in, std::ostream& out); + friend void write_parser_rdf(std::ostream& out, bool with_prefixes); }; } diff --git a/src/SystemParser.hh b/src/SystemParser.hh index dd4b870..3b895fb 100644 --- a/src/SystemParser.hh +++ b/src/SystemParser.hh @@ -80,7 +80,7 @@ namespace archetype { void matchVerbs_(std::list& wordValues); void matchNouns_(std::list& wordValues); - friend void inspect_universe(Storage& in, std::ostream& out); + friend void write_parser_rdf(std::ostream& out, bool with_prefixes); }; Storage& operator<<(Storage& out, const SystemParser& p); diff --git a/src/TestExpression.cc b/src/TestExpression.cc index 6aeb3d0..e2ac143 100644 --- a/src/TestExpression.cc +++ b/src/TestExpression.cc @@ -18,6 +18,8 @@ #include "Expression.hh" #include "Serialization.hh" #include "StringInput.hh" +#include "StringOutput.hh" +#include "ReadEvalPrintLoop.hh" #include "Universe.hh" using namespace std; @@ -327,11 +329,62 @@ namespace archetype { ARCHETYPE_TEST(expr7 != nullptr); } + void TestExpression::testListLiterals_() { + // A list literal uses square brackets and evaluates to a PairValue + // chain whose display round-trips to the same bracket form. + Expression list_expr = make_expr_from_str("[1 2 3]"); + ARCHETYPE_TEST(list_expr != nullptr); + Value list_val = list_expr->evaluate()->valueConversion(); + ostringstream list_out; + list_val->display(list_out); + ARCHETYPE_TEST_EQUAL(list_out.str(), string{"[1 2 3]"}); + + // The empty list still parses. + Expression empty_expr = make_expr_from_str("[]"); + ARCHETYPE_TEST(empty_expr != nullptr); + + // Nested list literals. + Expression nested_expr = make_expr_from_str("[[1 2] [3 4]]"); + ARCHETYPE_TEST(nested_expr != nullptr); + Value nested_val = nested_expr->evaluate()->valueConversion(); + ostringstream nested_out; + nested_val->display(nested_out); + ARCHETYPE_TEST_EQUAL(nested_out.str(), string{"[[1 2] [3 4]]"}); + + // Curly braces in expression position no longer form a list literal. + Expression curly_expr = make_expr_from_str("{1 2 3}"); + ARCHETYPE_TEST(curly_expr == nullptr); + } + + void TestExpression::testReplDisplay_() { + // Drive the REPL with a few inputs and verify each result is echoed + // once with the `=> ` prefix — the display form only, matching the + // Python/Ruby/Lisp convention. No duplicated stringConversion tail. + UserInput prior_input = Universe::instance().input(); + UserOutput prior_output = Universe::instance().output(); + UserInput repl_input{new StringInput{"3 + 5\n[1 2 3]\n\"hi\"\nexit\n"}}; + UserOutput repl_output{new StringOutput}; + Universe::instance().setInput(repl_input); + Universe::instance().setOutput(repl_output); + int errors = repl(); + ARCHETYPE_TEST_EQUAL(errors, 0); + string text = dynamic_cast(repl_output.get())->getOutput(); + ARCHETYPE_TEST(text.find("=> 8\n") != string::npos); + ARCHETYPE_TEST(text.find("=> 8 8") == string::npos); + ARCHETYPE_TEST(text.find("=> [1 2 3]\n") != string::npos); + ARCHETYPE_TEST(text.find("=> \"hi\"\n") != string::npos); + ARCHETYPE_TEST(text.find("\"hi\" hi") == string::npos); + Universe::instance().setInput(prior_input); + Universe::instance().setOutput(prior_output); + } + void TestExpression::runTests_() { testTranslation_(); testEvaluation_(); testSerialization_(); testInput_(); testVerification_(); + testListLiterals_(); + testReplDisplay_(); } } diff --git a/src/TestExpression.hh b/src/TestExpression.hh index 4db66bf..8ccf463 100644 --- a/src/TestExpression.hh +++ b/src/TestExpression.hh @@ -20,6 +20,8 @@ namespace archetype { void testSerialization_(); void testInput_(); void testVerification_(); + void testListLiterals_(); + void testReplDisplay_(); protected: virtual void runTests_() override; public: diff --git a/src/TestInspectUniverse.cc b/src/TestInspectUniverse.cc new file mode 100644 index 0000000..10c89ec --- /dev/null +++ b/src/TestInspectUniverse.cc @@ -0,0 +1,177 @@ +// +// 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_(bool include_methods = false) { + // Serialize the universe + MemoryStorage mem; + mem << Universe::instance(); + + // Inspect it + ostringstream ttl; + inspect_universe(mem, ttl, include_methods); + 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_(/* include_methods = */ true); + + // 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); + + // 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_() { + 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_(/* include_methods = */ true); + + // 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::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_(/* include_methods = */ true); + + // 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 new file mode 100644 index 0000000..cbdeb06 --- /dev/null +++ b/src/TestInspectUniverse.hh @@ -0,0 +1,29 @@ +// +// 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_(); + void testParserBlock_(); + protected: + virtual void runTests_() override; + public: + TestInspectUniverse(std::string name): ITestSuite(name) { } + }; +} + +#endif /* defined(__archetype__TestInspectUniverse__) */ diff --git a/src/TestUpdateUniverse.cc b/src/TestUpdateUniverse.cc new file mode 100644 index 0000000..5486737 --- /dev/null +++ b/src/TestUpdateUniverse.cc @@ -0,0 +1,137 @@ +// +// 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); + } + + // 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::testInspectAppendsStateRdf_() { + 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 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); + } + + void TestUpdateUniverse::runTests_() { + testPlainUpdate_(); + testSitrepAppendsParserRdf_(); + testSitrepUnpacksPairs_(); + testInspectAppendsStateRdf_(); + } +} diff --git a/src/TestUpdateUniverse.hh b/src/TestUpdateUniverse.hh new file mode 100644 index 0000000..b30216c --- /dev/null +++ b/src/TestUpdateUniverse.hh @@ -0,0 +1,29 @@ +// +// 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 testSitrepUnpacksPairs_(); + void testInspectAppendsStateRdf_(); + protected: + virtual void runTests_() override; + public: + TestUpdateUniverse(std::string name): ITestSuite(name) { } + }; +} + +#endif /* defined(__archetype__TestUpdateUniverse__) */ 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/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 fe9b250..1ce1bb4 100644 --- a/src/Value.cc +++ b/src/Value.cc @@ -18,6 +18,33 @@ 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) { + int identifier_id = Universe::instance().identifierForObject(object_id); + if (identifier_id < 0) return ""; + return Universe::instance().Identifiers.get(identifier_id); + } + + enum ValueType_e { UNDEFINED, ABSENT, @@ -99,6 +126,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 +143,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 +160,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 +179,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 +216,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 +254,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 +271,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 +298,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 +330,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_; } @@ -290,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 << "'; } + 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_; } Value ObjectValue::identifierConversion() const { - for (auto const& p : Universe::instance().ObjectIdentifiers) { - if (p.second == objectId_) { - return Value{new IdentifierValue{p.first}}; - } + int identifier_id = Universe::instance().identifierForObject(objectId_); + if (identifier_id >= 0) { + return Value{new IdentifierValue{identifier_id}}; } return Value{new UndefinedValue}; } @@ -330,6 +401,10 @@ namespace archetype { return other_p and other_p->objectId_ == objectId_ and other_p->attributeId_ == attributeId_; } + std::string AttributeValue::asRDF() const { + return dereference_()->asRDF(); + } + void AttributeValue::display(std::ostream &out) const { Value obj_v{new ObjectValue{objectId_}}; obj_v->display(out); @@ -418,7 +493,7 @@ namespace archetype { tail_->display(out); out << ')'; } else { - out << '{'; + out << '['; head_->display(out); while (tail_p) { out << ' '; @@ -433,8 +508,27 @@ namespace archetype { break; } } - out << '}'; + out << ']'; + } + } + + std::string PairValue::asRDF() const { + // Render as a Turtle collection: ( item1 item2 ... ) + std::string result = "( "; + result += head_->asRDF(); + const PairValue* p = dynamic_cast(tail_.get()); + while (p) { + result += " "; + result += p->head_->asRDF(); + const PairValue* next = dynamic_cast(p->tail_.get()); + if (not next and p->tail_->isDefined()) { + result += " "; + result += p->tail_->asRDF(); + } + p = next; } + result += " )"; + return result; } void PairValue::write(Storage &out) const { diff --git a/src/Value.hh b/src/Value.hh index c7c177c..f50e445 100644 --- a/src/Value.hh +++ b/src/Value.hh @@ -36,6 +36,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const = 0; virtual Value clone() const = 0; virtual void display(std::ostream& out) const = 0; + virtual std::string asRDF() const = 0; virtual void write(Storage& out) const = 0; virtual bool isTrueEnough() const { return true; } @@ -65,6 +66,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new UndefinedValue}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isDefined() const override { return false; } @@ -77,6 +79,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new AbsentValue}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isDefined() const override { return true; } @@ -89,6 +92,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new BreakValue}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isDefined() const override { return true; } @@ -102,6 +106,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new BooleanValue(value_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual bool isTrueEnough() const override { return value_; } @@ -117,6 +122,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new MessageValue{message_}}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getMessage() const override; @@ -133,6 +139,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new TextLiteralValue{textLiteral_}}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual std::string getString() const override; @@ -150,6 +157,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new NumericValue(value_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getNumber() const override; @@ -166,6 +174,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new StringValue(value_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual std::string getString() const override; @@ -183,6 +192,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new IdentifierValue(id_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getIdentifier() const override; @@ -198,6 +208,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value{new ObjectValue{objectId_}}; } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getObject() const override; @@ -220,6 +231,7 @@ namespace archetype { virtual bool isSameValueAs(const Value& other) const override; virtual Value clone() const override { return Value(new AttributeValue(objectId_, attributeId_)); } virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; virtual int getIdentifier() const override; @@ -253,6 +265,7 @@ namespace archetype { virtual Value tail() const override; virtual void display(std::ostream& out) const override; + virtual std::string asRDF() const override; virtual void write(Storage& out) const override; }; diff --git a/src/inspect_universe.cc b/src/inspect_universe.cc index 8bddcb4..281a243 100644 --- a/src/inspect_universe.cc +++ b/src/inspect_universe.cc @@ -2,157 +2,231 @@ #include #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); + + // 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; + 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(); + } + + 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(identifier_id); + } + + // 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(); + } + + 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" + << "@prefix xsd: .\n\n" + << "@prefix archetype: .\n" + << "@prefix type: .\n" + << "@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); + + // -- 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_for(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); + } } - }; - for (auto const& a : Universe::instance().ObjectIdentifiers) { - reverse_ids.insert(std::make_pair(a.second, a.first)); + 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_for(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) { + write_rdf_prefixes(out); + 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_for(obj_id); + ObjectPtr parent = obj->parent(); if (parent) { if (obj->isPrototype()) { out << " a rdfs:Class\n" - << " ; rdfs:subClassOf "; + << " ; rdfs:subClassOf " << obj_name_for(parent->id()); } else { - out << " a "; + out << " a " << obj_name_for(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_for(Universe::NullObjectId); } - out << '\n'; - // This is the flat, not the inherited, view of the attributes and methods. - // Only what is added by each type. + 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'; + 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(); } } - 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); - } + + 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? - 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. - - 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(); - }; - 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); - } - 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); + out << " .\n\n"; } - 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); - } - 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; - } - ss << ']'; - return ss.str(); - }; - out << "{\"verbs\": " << json_list(all_verb_phrases) << ",\n"; - out << "\"nouns\": " << json_list(all_noun_phrases) << "}\n"; - } // inspect_universe -} \ No newline at end of file + if (include_methods) write_parser_rdf(out, /* with_prefixes = */ false); + } + + 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 2089327..a866420 100644 --- a/src/inspect_universe.hh +++ b/src/inspect_universe.hh @@ -14,7 +14,17 @@ namespace archetype { - void inspect_universe(Storage& in, std::ostream& out); + // 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); } diff --git a/src/main.cc b/src/main.cc index bebcab7..57458f5 100644 --- a/src/main.cc +++ b/src/main.cc @@ -89,18 +89,26 @@ 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 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 ; } 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); @@ -112,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); @@ -159,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()) { @@ -181,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; @@ -196,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"; } @@ -209,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; @@ -232,8 +271,24 @@ int main(int argc, const char* argv[]) { } copy(istreambuf_iterator{f_in}, {}, back_inserter(in_mem.bytes())); } + 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 = 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); + 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); @@ -243,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"; } @@ -253,12 +309,15 @@ 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); + 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; } diff --git a/src/update_universe.cc b/src/update_universe.cc index 43cf1a3..376cc8e 100644 --- a/src/update_universe.cc +++ b/src/update_universe.cc @@ -4,10 +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 "inspect_universe.hh" + +#include namespace archetype { @@ -44,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) { @@ -61,7 +103,8 @@ Value dispatch_to_universe(string message) { return result; } -string update_universe(Storage& in, Storage& out, string input, int width) { +string update_universe(Storage& in, Storage& out, string input, int width, + bool sitrep, bool inspect) { // Paging, no; wrapping, yes. UserOutput str_output{new StringOutput}; UserOutput wrapped{new WrappedOutput{str_output, width}}; @@ -76,8 +119,32 @@ 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()) { + ostringstream rdf_out; + write_parser_rdf(rdf_out, /* with_prefixes = */ true); + 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 + } + } + 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(); - 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..00ee91a 100644 --- a/src/update_universe.hh +++ b/src/update_universe.hh @@ -16,8 +16,10 @@ 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, + bool inspect = false); + } #endif