载入中...
搜索中...
未找到
ConversationDialect.cpp
浏览该文件的文档.
2
5
6#include <cstdlib>
7#include <stdexcept>
8#include <string>
9#include <unordered_set>
10#include <utility>
11#include <vector>
12
13namespace eve::dnut {
14namespace {
15
16class ConversationParseError final : public std::runtime_error {
17public:
18 ConversationParseError(const DnutToken& token, std::string message)
19 : std::runtime_error(std::move(message)), line(token.line), column(token.column) {}
20
21 int line;
22 int column;
23};
24
25eve::Value numberValue(const std::string& raw) {
26 if (raw.find_first_of(".eE") != std::string::npos) return eve::Value::number(std::strtod(raw.c_str(), nullptr));
27 return eve::Value::integer(std::strtoll(raw.c_str(), nullptr, 10));
28}
29
30class ConversationBlockParser {
31public:
32 ConversationBlockParser(const std::vector<DnutToken>& tokens, const DnutBlock& block)
33 : tokens_(tokens), end_(block.endToken), index_(block.beginToken) {}
34
35 SequenceAsset parse() {
36 const DnutToken declaration = cur();
37 expectKeyword("conversation");
38 SequenceAsset asset;
39 asset.sourceLine = declaration.line;
40 asset.sourceColumn = declaration.column;
41 asset.id = expectIdentifier("a conversation id");
42 while (!isPunct("{")) {
43 const std::string key = expectIdentifier("a conversation field");
44 expectPunct("=");
45 if (key == "entry")
46 asset.entry = scalarText();
47 else if (key == "version") {
48 if (cur().kind != DnutTokenKind::Number) fail("version requires an integer");
49 asset.version = static_cast<int>(std::strtol(advance().text.c_str(), nullptr, 10));
50 } else {
51 fail("unknown conversation field '" + key + "'");
52 }
53 }
54 expectPunct("{");
55 while (!isPunct("}")) {
56 if (atEnd()) fail("unterminated conversation block");
57 if (isIdentifier("parameter"))
58 asset.parameters.push_back(parseParameter());
59 else if (isIdentifier("node"))
60 asset.nodes.push_back(parseNode());
61 else
62 fail("conversation accepts only parameter or node declarations");
63 }
64 expectPunct("}");
65 return asset;
66 }
67
68private:
69 const DnutToken& cur() const {
70 const std::size_t position = index_ < tokens_.size() ? index_ : tokens_.size() - 1;
71 return tokens_[position];
72 }
73 bool atEnd() const { return index_ >= end_ || cur().kind == DnutTokenKind::EndOfFile; }
74 DnutToken advance() {
75 const DnutToken token = cur();
76 if (!atEnd()) ++index_;
77 return token;
78 }
79 bool isPunct(const char* text) const { return cur().kind == DnutTokenKind::Punctuator && cur().text == text; }
80 bool isIdentifier(const char* text) const { return cur().kind == DnutTokenKind::Identifier && cur().text == text; }
81 [[noreturn]] void fail(std::string message) const { throw ConversationParseError(cur(), std::move(message)); }
82 void expectPunct(const char* text) {
83 if (!isPunct(text)) fail("expected '" + std::string(text) + "'");
84 advance();
85 }
86 void expectKeyword(const char* text) {
87 if (!isIdentifier(text)) fail("expected '" + std::string(text) + "'");
88 advance();
89 }
90 std::string expectIdentifier(const std::string& description) {
91 if (cur().kind != DnutTokenKind::Identifier) fail("expected " + description);
92 return advance().text;
93 }
94 std::string scalarText() {
97 fail("expected a scalar value");
98 return advance().text;
99 }
100
101 eve::Value parseLiteral() {
102 if (isPunct("-")) {
103 advance();
104 if (cur().kind != DnutTokenKind::Number) fail("'-' must be followed by a number");
105 eve::Value value = numberValue(advance().text);
106 return value.isInt64() ? eve::Value::integer(-value.asInt()) : eve::Value::number(-value.asDouble());
107 }
108 if (cur().kind == DnutTokenKind::String) return eve::Value::string(advance().text);
109 if (cur().kind == DnutTokenKind::Number) return numberValue(advance().text);
110 if (cur().kind == DnutTokenKind::Identifier) {
111 if (isIdentifier("true")) {
112 advance();
113 return eve::Value::boolean(true);
114 }
115 if (isIdentifier("false")) {
116 advance();
117 return eve::Value::boolean(false);
118 }
119 return eve::Value::string(advance().text);
120 }
121 fail("expected a literal value");
122 }
123
124 eve::Value parseObjectArguments() {
125 expectPunct("(");
127 while (!isPunct(")")) {
128 const std::string key = expectIdentifier("an argument name");
129 expectPunct("=");
130 fields.emplace(key, parseLiteral());
131 if (isPunct(","))
132 advance();
133 else if (!isPunct(")"))
134 fail("expected ',' or ')' in argument list");
135 }
136 advance();
137 return eve::Value(std::move(fields));
138 }
139
140 SequenceParameter parseParameter() {
141 const DnutToken declaration = advance();
142 SequenceParameter parameter;
143 parameter.sourceLine = declaration.line;
144 parameter.sourceColumn = declaration.column;
145 parameter.name = expectIdentifier("a parameter name");
146 const std::string type = expectIdentifier("a parameter type");
147 if (type == "string")
148 parameter.type = SequenceParameterType::String;
149 else if (type == "int")
150 parameter.type = SequenceParameterType::Integer;
151 else if (type == "float")
152 parameter.type = SequenceParameterType::Number;
153 else if (type == "bool")
154 parameter.type = SequenceParameterType::Boolean;
155 else
156 fail("unknown parameter type '" + type + "'");
157
158 while (!atEnd() && cur().line == declaration.line) {
159 const std::string field = expectIdentifier("a parameter field");
160 if (field == "required")
161 parameter.required = true;
162 else if (field == "optional")
163 parameter.required = false;
164 else if (field == "default") {
165 expectPunct("=");
166 parameter.defaultValue = parseLiteral();
167 parameter.required = false;
168 } else {
169 fail("unknown parameter field '" + field + "'");
170 }
171 }
172 return parameter;
173 }
174
175 void appendMutation(eve::Value& payload, eve::Value mutation) {
176 eve::Value* mutations = payload.find("stateMutations");
177 if (!mutations) {
178 payload.set("stateMutations", eve::Value(eve::Value::Array{}));
179 mutations = payload.find("stateMutations");
180 }
181 mutations->pushBack(std::move(mutation));
182 }
183
184 SequenceRoute parseRoute() {
185 const DnutToken declaration = advance();
186 SequenceRoute route;
187 route.sourceLine = declaration.line;
188 route.sourceColumn = declaration.column;
189 route.label = expectIdentifier("a stable route id");
190 bool hasTarget = false;
191 while (!atEnd() && cur().line == declaration.line && !isPunct("}")) {
192 const std::string key = expectIdentifier("a route field");
193 if (key == "payment") {
194 route.payload.set("payment", parseObjectArguments());
195 continue;
196 }
197 if (key == "mutation") {
198 appendMutation(route.payload, parseObjectArguments());
199 continue;
200 }
201 expectPunct("=");
202 eve::Value value = parseLiteral();
203 if (key == "target") {
204 if (!value.isString()) fail("route target must be a node id");
205 route.target = value.asString();
206 hasTarget = true;
207 } else if (key == "when") {
208 route.condition = std::move(value);
209 } else if (key == "text" || key == "i18n") {
210 route.payload.set(key, std::move(value));
211 } else {
212 fail("unknown route field '" + key + "'");
213 }
214 }
215 if (!hasTarget) fail("route requires target");
216 return route;
217 }
218
219 SequenceNode parseNode() {
220 const DnutToken declaration = advance();
221 SequenceNode node;
222 node.sourceLine = declaration.line;
223 node.sourceColumn = declaration.column;
224 node.id = expectIdentifier("a node id");
225 node.type = expectIdentifier("a node type");
226
227 while (!atEnd() && cur().line == declaration.line && !isPunct("{") && !isPunct("}")) {
228 const std::string key = expectIdentifier("a node field");
229 if (key == "arguments") {
230 node.payload.set("arguments", parseObjectArguments());
231 continue;
232 }
233 if (key == "payment") {
234 node.payload.set("payment", parseObjectArguments());
235 continue;
236 }
237 if (key == "mutation") {
238 appendMutation(node.payload, parseObjectArguments());
239 continue;
240 }
241 expectPunct("=");
242 eve::Value value = parseLiteral();
243 if (key == "next") {
244 if (!value.isString()) fail("next must be a node id");
245 node.next = value.asString();
246 } else if (key == "target" && node.type == "command") {
247 node.payload.set("name", std::move(value));
248 } else if (key == "result") {
249 node.payload.set("resultLocal", std::move(value));
250 } else {
251 node.payload.set(key, std::move(value));
252 }
253 }
254 if (isPunct("{")) {
255 advance();
256 while (!isPunct("}")) {
257 if (atEnd()) fail("unterminated node block");
258 if (!isIdentifier("route")) fail("node block accepts only route declarations");
259 node.routes.push_back(parseRoute());
260 }
261 advance();
262 }
263 return node;
264 }
265
266 const std::vector<DnutToken>& tokens_;
267 std::size_t end_;
268 std::size_t index_;
269};
270
271void appendResultFailure(DnutCompileOutput& output, const std::string& path, const eve::Diagnostic* diagnostic,
272 const char* defaultMessage) {
273 int line = 0;
274 int column = 0;
275 if (diagnostic) {
276 for (const auto& [key, value] : diagnostic->details()) {
277 if (key == "line") line = std::atoi(value.c_str());
278 if (key == "column") column = std::atoi(value.c_str());
279 }
280 }
281 output.diagnostics.push_back(
282 {DnutSeverity::Error, path, line, column, diagnostic ? diagnostic->message() : defaultMessage});
283}
284
285bool validateDocumentEnvelope(const std::vector<DnutToken>& tokens, const std::string& path,
286 DnutCompileOutput& output) {
287 if (tokens.size() < 5 || tokens[0].kind != DnutTokenKind::Identifier || tokens[0].text != "schema" ||
288 tokens[1].kind != DnutTokenKind::String || tokens[1].text != "eve.dnut" ||
289 tokens[2].kind != DnutTokenKind::Identifier || tokens[2].text != "version" ||
290 tokens[3].kind != DnutTokenKind::Number || std::strtol(tokens[3].text.c_str(), nullptr, 10) != 1) {
291 const DnutToken& token = tokens.empty() ? DnutToken{} : tokens.front();
292 output.diagnostics.push_back(
293 {DnutSeverity::Error, path, token.line, token.column, "expected schema \"eve.dnut\" and version 1"});
294 return false;
295 }
296 return true;
297}
298
299} // namespace
300
301DnutCompileOutput compileDnutConversations(std::string_view source, const std::string& path,
302 const StepKindRegistry& registry) {
304 auto tokensResult = lexDnut(source, path);
305 if (!tokensResult.ok()) {
306 appendResultFailure(output, path, tokensResult.error(), "could not lex the document");
307 return output;
308 }
309 std::vector<DnutToken> tokens = std::move(tokensResult).takeValue();
310 if (!validateDocumentEnvelope(tokens, path, output)) return output;
311
312 auto blocksResult = scanDnutBlocks(tokens, path);
313 if (!blocksResult.ok()) {
314 appendResultFailure(output, path, blocksResult.error(), "could not scan the document");
315 return output;
316 }
317
318 std::unordered_set<std::string> assetIds;
319 for (const auto& block : blocksResult.value()) {
320 if (block.kind != "conversation") continue;
321 try {
322 SequenceAsset asset = ConversationBlockParser(tokens, block).parse();
323 bool valid = true;
324 if (!assetIds.insert(asset.id).second) {
325 output.diagnostics.push_back({DnutSeverity::Error, path, asset.sourceLine, asset.sourceColumn,
326 "duplicate conversation id '" + asset.id + "'"});
327 valid = false;
328 }
329 if (auto graph = asset.validate(); !graph.ok()) {
330 output.diagnostics.push_back({DnutSeverity::Error, path, asset.sourceLine, asset.sourceColumn,
331 graph.error() ? graph.error()->message() : "invalid conversation graph"});
332 valid = false;
333 }
334 for (const auto& node : asset.nodes) {
335 if (isCoreSequenceNodeType(node.type) && !registry.contains(node.type)) continue;
336 auto validated = registry.validate(node);
337 if (!validated.ok()) {
338 output.diagnostics.push_back(
339 {DnutSeverity::Error, path, node.sourceLine, node.sourceColumn,
340 validated.error() ? validated.error()->message() : "invalid conversation node"});
341 valid = false;
342 }
343 }
344 if (valid) output.assets.push_back(std::move(asset));
345 } catch (const ConversationParseError& error) {
346 output.diagnostics.push_back({DnutSeverity::Error, path, error.line, error.column, error.what()});
347 }
348 }
349 return output;
350}
351
352} // namespace eve::dnut
double value
Value::Object payload
std::string output
Shared top-level block discovery for `.dnut` dialects.
Registry-driven compiler for the `.dnut` story dialect.
Shared lexer for every `.dnut` dialect.
int column
std::string message
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::uint32_t key
std::string text
TokenKind kind
std::array< float, 3 > position
std::string description
bool valid
std::string error
Definition Package.cpp:60
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
uint64_t token
const RoadNode * node
double number
const UnitySourceAsset & source
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
const std::string & message() const noexcept
Human-readable explanation.
Definition Diagnostic.h:147
const std::vector< Diagnostic > & diagnostics() const noexcept
Inspect all diagnostics; this counts as checking the Result.
Definition Result.h:501
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
void set(const std::string &key, Value value)
Insert or replace an object member.
Definition Value.cpp:118
static Value number(double value)
Compatibility factory for a Double value.
Definition Value.h:106
static Value string(std::string value)
Compatibility factory for a string value.
Definition Value.h:110
std::vector< Value > Array
Definition Value.h:33
static Value integer(std::int64_t value)
Compatibility factory for an Int64 value.
Definition Value.h:104
void pushBack(Value value)
Append an element to an array value.
Definition Value.cpp:94
static Value boolean(bool value)
Compatibility factory for a boolean value.
Definition Value.h:108
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
Canonical step vocabulary owner and runtime handler router.
eve::Result< void > validate(const SequenceNode &node) const
Validate one node against its declared payload schema.
bool contains(std::string_view type) const
Return whether type is declared.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
eve::Result< std::vector< DnutBlock > > scanDnutBlocks(const std::vector< DnutToken > &tokens, const std::string &path)
Discover balanced top-level dialect blocks in one lexed document.
eve::Result< std::vector< DnutToken > > lexDnut(std::string_view source, const std::string &path)
Lex one .dnut source buffer into owned tokens.
Definition DnutLexer.cpp:97
bool isCoreSequenceNodeType(const std::string &type) noexcept
Return whether type is a control-flow type interpreted by the runtime.
@ String
Quoted string with escapes already resolved.
@ Identifier
Bare word: keyword, field name, identifier, or bare enum value.
@ Punctuator
One- or two-character punctuator such as {, =, ==, ->.
@ EndOfFile
Synthetic terminal token; always the last element.
@ Number
Unsigned numeric literal; sign handling belongs to the parser.
DnutCompileOutput compileDnutConversations(std::string_view source, const std::string &path, const StepKindRegistry &registry)
Compile legacy conversation blocks into canonical sequence assets.
const EditorValue * field(const EditorValue &value, const char *name)
ModelData parse(std::span< const uint8_t > bytes)
Parse.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Owning outcome of compiling one .dnut document.
One lexed token with its exact source anchor.
Definition DnutLexer.h:31
Immutable, parameterized and versioned compiled sequence.
eve::Result< void > validate() const
Validate stable identifiers, the entry point and every graph reference.
std::vector< SequenceNode > nodes