载入中...
搜索中...
未找到
DnutCompiler.cpp
浏览该文件的文档.
2
4
5#include <cstdlib>
6#include <string>
7#include <unordered_set>
8#include <utility>
9#include <vector>
10
11namespace eve::dnut {
12
14
15namespace {
16
18struct ParseAbort {};
19
20struct CompiledOption;
21
22struct CompiledStatement {
23 enum class Kind { Step, If, Choice };
24
25 Kind kind = Kind::Step;
26 std::string nodeId;
28 std::string type;
31 std::vector<CompiledStatement> thenBody;
32 std::vector<CompiledStatement> elseBody;
33 std::vector<CompiledOption> options;
34};
35
37struct CompiledOption {
38 std::string label;
40 std::vector<CompiledStatement> body;
41 std::string nodeId;
42};
43
44eve::Value numberValue(const std::string& raw) {
45 if (raw.find_first_of(".eE") != std::string::npos) return eve::Value::number(std::strtod(raw.c_str(), nullptr));
46 return eve::Value::integer(std::strtoll(raw.c_str(), nullptr, 10));
47}
48
49bool isComparisonOperator(const std::string& text) {
50 return text == "==" || text == "!=" || text == ">" || text == "<" || text == ">=" || text == "<=";
51}
52
53const char* comparisonSpelling(const std::string& text) {
54 if (text == "==") return "eq";
55 if (text == "!=") return "ne";
56 if (text == ">") return "gt";
57 if (text == "<") return "lt";
58 if (text == ">=") return "ge";
59 return "le";
60}
61
62class StoryParser {
63public:
64 StoryParser(std::vector<DnutToken> tokens, std::string path, const StepKindRegistry& registry,
65 DnutCompileOutput& output)
66 : tokens_(std::move(tokens)), path_(std::move(path)), registry_(registry), output_(output) {}
67
68 void run() {
69 while (!atEnd()) {
70 if (isIdentifier("story")) {
71 try {
72 parseStory();
73 } catch (const ParseAbort&) {
74 synchronize();
75 }
76 continue;
77 }
78 skipForeignBlock();
79 }
80 }
81
82private:
83 const DnutToken& cur() const { return tokens_[index_]; }
84 const DnutToken& peek(std::size_t offset) const {
85 const std::size_t position = index_ + offset;
86 return tokens_[position < tokens_.size() ? position : tokens_.size() - 1];
87 }
88 bool atEnd() const { return cur().kind == DnutTokenKind::EndOfFile; }
90 DnutToken advance() {
91 const DnutToken token = tokens_[index_];
92 if (!atEnd()) ++index_;
93 return token;
94 }
95 bool isPunct(const std::string& text) const {
96 return cur().kind == DnutTokenKind::Punctuator && cur().text == text;
97 }
98 bool isIdentifier(const std::string& text) const {
99 return cur().kind == DnutTokenKind::Identifier && cur().text == text;
100 }
101
102 [[noreturn]] void error(const DnutToken& token, std::string message) {
103 output_.diagnostics.push_back(
104 {DnutSeverity::Error, path_, token.line, token.column, std::move(message)});
105 errorLine_ = token.line;
106 throw ParseAbort{};
107 }
108 [[noreturn]] void errorHere(std::string message) { error(cur(), std::move(message)); }
109
110 void expectPunct(const std::string& text) {
111 if (!isPunct(text)) errorHere("expected '" + text + "'");
112 advance();
113 }
114 std::string expectIdentifier(const std::string& what) {
115 if (cur().kind != DnutTokenKind::Identifier) errorHere("expected " + what);
116 return advance().text;
117 }
118 std::string nextNodeId() { return "n" + std::to_string(++nodeCounter_); }
119
121 void synchronize() {
122 int depth = 0;
123 bool consumed = false;
124 while (!atEnd()) {
125 if (depth == 0) {
126 if (isPunct("}")) return;
127 if (consumed && cur().kind == DnutTokenKind::Identifier && cur().line > errorLine_) return;
128 }
129 if (isPunct("{")) {
130 ++depth;
131 } else if (isPunct("}")) {
132 if (depth == 0) return;
133 --depth;
134 }
135 advance();
136 consumed = true;
137 }
138 }
139
141 void skipForeignBlock() {
142 int depth = 0;
143 while (!atEnd()) {
144 if (depth == 0 && isIdentifier("story")) return;
145 if (isPunct("{")) {
146 ++depth;
147 } else if (isPunct("}")) {
148 if (depth > 0) --depth;
149 }
150 advance();
151 }
152 }
153
154 void parseStory() {
155 advance(); // 'story'
156 invalidStep_ = false;
157 const std::string id = expectIdentifier("a story id");
158 bool repeatable = false;
159 int version = 1;
160 while (cur().kind == DnutTokenKind::Identifier) {
161 const std::string modifier = cur().text;
162 if (modifier == "repeatable") {
163 repeatable = true;
164 advance();
165 continue;
166 }
167 if (modifier == "version") {
168 advance();
169 expectPunct("=");
170 if (cur().kind != DnutTokenKind::Number) errorHere("version requires an integer");
171 version = static_cast<int>(std::strtol(advance().text.c_str(), nullptr, 10));
172 if (version < 1) errorHere("version must be positive");
173 continue;
174 }
175 errorHere("unknown story modifier '" + modifier + "'");
176 }
177
178 std::vector<CompiledStatement> body;
179 parseBlockBody(body);
180
181 SequenceAsset asset;
182 asset.id = id;
183 asset.version = version;
184 asset.repeatable = repeatable;
185 const std::string endId = nextNodeId();
186 SequenceNode endNode;
187 endNode.id = endId;
188 endNode.type = "end";
189 const std::string entry = emitBlock(body, endId, asset);
190 asset.entry = entry.empty() ? endId : entry;
191 asset.nodes.push_back(std::move(endNode));
192
193 bool publishable = !invalidStep_;
194 if (auto validated = asset.validate(); !validated.ok()) {
195 const auto* diagnostic = validated.error();
196 output_.diagnostics.push_back(
197 {DnutSeverity::Error, path_, 0, 0, diagnostic ? diagnostic->message() : "invalid sequence asset"});
198 publishable = false;
199 }
200 if (!publishedIds_.insert(asset.id).second) {
201 output_.diagnostics.push_back(
202 {DnutSeverity::Error, path_, 0, 0, "duplicate story id '" + asset.id + "'"});
203 publishable = false;
204 }
205 if (publishable) output_.assets.push_back(std::move(asset));
206 }
207
208 void parseBlockBody(std::vector<CompiledStatement>& out) {
209 expectPunct("{");
210 while (true) {
211 if (atEnd()) errorHere("unterminated block");
212 if (isPunct("}")) {
213 advance();
214 return;
215 }
216 try {
217 out.push_back(parseStatement());
218 } catch (const ParseAbort&) {
219 synchronize();
220 }
221 }
222 }
223
224 CompiledStatement parseStatement() {
225 if (cur().kind != DnutTokenKind::Identifier) errorHere("expected a statement");
226 const std::string keyword = cur().text;
227 if (keyword == "if") return parseIf();
228 if (keyword == "choice") return parseChoice();
229 if (keyword == "wait") return parseWait();
230 if (keyword == "call") return parseCall();
231 if (keyword == "end") {
232 advance();
233 CompiledStatement statement;
234 statement.type = "end";
235 statement.nodeId = nextNodeId();
236 return statement;
237 }
238 return parseStep();
239 }
240
241 CompiledStatement parseStep() {
242 const DnutToken start = cur();
243 CompiledStatement statement;
244 statement.type = advance().text;
245 statement.nodeId = nextNodeId();
246 while (cur().kind == DnutTokenKind::Identifier && peek(1).kind == DnutTokenKind::Punctuator &&
247 peek(1).text == "=") {
248 const std::string field = advance().text;
249 advance(); // '='
250 statement.payload.set(field, parseValue());
251 }
252
253 // Validate against the vocabulary here, while the statement's own source
254 // location is still available, so a bad field points at the authored line
255 // instead of at the whole document.
256 SequenceNode candidate;
257 candidate.id = statement.nodeId;
258 candidate.type = statement.type;
259 candidate.payload = statement.payload;
260 if (auto validated = registry_.validate(candidate); !validated.ok()) {
261 const auto* diagnostic = validated.error();
262 output_.diagnostics.push_back({DnutSeverity::Error, path_, start.line, start.column,
263 diagnostic ? diagnostic->message() : "invalid step"});
264 invalidStep_ = true;
265 }
266 return statement;
267 }
268
269 CompiledStatement parseWait() {
270 advance(); // 'wait'
271 CompiledStatement statement;
272 statement.type = "wait";
273 statement.nodeId = nextNodeId();
274 if (cur().kind == DnutTokenKind::Number) {
275 statement.payload.set("duration", numberValue(advance().text));
276 return statement;
277 }
278 if (isIdentifier("duration")) {
279 advance();
280 expectPunct("=");
281 statement.payload.set("duration", parseValue());
282 return statement;
283 }
284 errorHere("wait requires a duration");
285 }
286
287 CompiledStatement parseCall() {
288 advance(); // 'call'
289 CompiledStatement statement;
290 statement.type = "call";
291 statement.nodeId = nextNodeId();
292 statement.payload.set("target", eve::Value::string(expectIdentifier("a called story id")));
293 return statement;
294 }
295
296 CompiledStatement parseIf() {
297 advance(); // 'if'
298 CompiledStatement statement;
299 statement.kind = CompiledStatement::Kind::If;
300 statement.nodeId = nextNodeId();
301 statement.condition = parseCondition();
302 parseBlockBody(statement.thenBody);
303 if (isIdentifier("else")) {
304 advance();
305 parseBlockBody(statement.elseBody);
306 }
307 return statement;
308 }
309
310 CompiledStatement parseChoice() {
311 advance(); // 'choice'
312 CompiledStatement statement;
313 statement.kind = CompiledStatement::Kind::Choice;
314 statement.nodeId = nextNodeId();
315 expectPunct("{");
316 while (!isPunct("}")) {
317 if (atEnd()) errorHere("unterminated choice block");
318 if (!isIdentifier("option")) errorHere("choice accepts only option statements");
319 advance();
320 CompiledOption option;
321 option.nodeId = nextNodeId();
322 if (cur().kind != DnutTokenKind::String) errorHere("option requires a quoted label");
323 option.label = advance().text;
324 if (isIdentifier("when")) {
325 advance();
326 option.condition = parseCondition();
327 }
328 parseBlockBody(option.body);
329 statement.options.push_back(std::move(option));
330 }
331 advance(); // '}'
332 if (statement.options.empty()) errorHere("choice requires at least one option");
333 return statement;
334 }
335
336 eve::Value parseCondition() { return parseOr(); }
337
338 eve::Value parseOr() {
339 eve::Value left = parseAnd();
340 while (isPunct("||")) {
341 advance();
342 eve::Value right = parseAnd();
343 if (auto* object = left.getIf<eve::Value::Object>();
344 object != nullptr && object->contains("any") && object->at("any").isArray()) {
345 object->at("any").pushBack(std::move(right));
346 } else {
348 {"any", eve::Value(eve::Value::Array{std::move(left), std::move(right)})}});
349 }
350 }
351 return left;
352 }
353
354 eve::Value parseAnd() {
355 eve::Value left = parseNot();
356 while (isPunct("&&")) {
357 advance();
358 eve::Value right = parseNot();
359 if (auto* object = left.getIf<eve::Value::Object>();
360 object != nullptr && object->contains("all") && object->at("all").isArray()) {
361 object->at("all").pushBack(std::move(right));
362 } else {
364 {"all", eve::Value(eve::Value::Array{std::move(left), std::move(right)})}});
365 }
366 }
367 return left;
368 }
369
370 eve::Value parseNot() {
371 if (isPunct("!")) {
372 advance();
373 return eve::Value(eve::Value::Object{{"not", parseNot()}});
374 }
375 if (isPunct("(")) {
376 advance();
377 eve::Value inner = parseOr();
378 expectPunct(")");
379 return inner;
380 }
381 return parseComparison();
382 }
383
384 eve::Value parseComparison() {
385 if (cur().kind != DnutTokenKind::Identifier) errorHere("condition must start with a variable name");
386 const std::string variable = advance().text;
387 if (cur().kind != DnutTokenKind::Punctuator || !isComparisonOperator(cur().text))
388 errorHere("condition requires a comparison operator (== != > < >= <=)");
389 const std::string operation = advance().text;
390 eve::Value expected = parseValue();
391 return eve::Value(eve::Value::Object{{"var", eve::Value::string(variable)},
392 {"op", eve::Value::string(comparisonSpelling(operation))},
393 {"value", std::move(expected)}});
394 }
395
396 eve::Value parseValue() {
397 if (isPunct("-")) {
398 advance();
399 if (cur().kind != DnutTokenKind::Number) errorHere("'-' must be followed by a number");
400 eve::Value magnitude = numberValue(advance().text);
401 if (magnitude.isInt64()) return eve::Value::integer(-magnitude.asInt());
402 return eve::Value::number(-magnitude.asDouble());
403 }
404 switch (cur().kind) {
405 case DnutTokenKind::String: return eve::Value::string(advance().text);
406 case DnutTokenKind::Number: return numberValue(advance().text);
408 if (isIdentifier("true")) {
409 advance();
410 return eve::Value::boolean(true);
411 }
412 if (isIdentifier("false")) {
413 advance();
414 return eve::Value::boolean(false);
415 }
416 return eve::Value::string(advance().text);
417 default: errorHere("expected a literal value"); break;
418 }
419 }
420
421 std::string emitBlock(const std::vector<CompiledStatement>& statements, const std::string& next,
422 SequenceAsset& asset) {
423 std::string continuation = next;
424 for (auto it = statements.rbegin(); it != statements.rend(); ++it)
425 continuation = emitStatement(*it, continuation, asset);
426 return continuation;
427 }
428
429 std::string emitStatement(const CompiledStatement& statement, const std::string& next, SequenceAsset& asset) {
430 if (statement.kind == CompiledStatement::Kind::Step) {
431 SequenceNode node;
432 node.id = statement.nodeId;
433 node.type = statement.type;
434 node.next = next;
435 node.payload = statement.payload;
436 asset.nodes.push_back(std::move(node));
437 return statement.nodeId;
438 }
439 if (statement.kind == CompiledStatement::Kind::If) {
440 const std::string thenEntry = emitBlock(statement.thenBody, next, asset);
441 const std::string elseEntry = emitBlock(statement.elseBody, next, asset);
442 SequenceNode node;
443 node.id = statement.nodeId;
444 node.type = "branch";
445 node.next = elseEntry;
446 SequenceRoute route;
447 route.condition = statement.condition;
448 route.target = thenEntry;
449 node.routes.push_back(std::move(route));
450 asset.nodes.push_back(std::move(node));
451 return statement.nodeId;
452 }
453 SequenceNode node;
454 node.id = statement.nodeId;
455 node.type = "choice";
456 node.next = next;
457 for (const auto& option : statement.options) {
458 SequenceRoute route;
459 route.label = option.label;
460 route.condition = option.condition;
461 route.target = emitBlock(option.body, next, asset);
462 node.routes.push_back(std::move(route));
463 }
464 asset.nodes.push_back(std::move(node));
465 return statement.nodeId;
466 }
467
468 std::vector<DnutToken> tokens_;
469 std::size_t index_ = 0;
470 std::string path_;
471 const StepKindRegistry& registry_;
472 DnutCompileOutput& output_;
473 int nodeCounter_ = 0;
474 int errorLine_ = 0;
476 bool invalidStep_ = false;
477 std::unordered_set<std::string> publishedIds_;
478};
479
480} // namespace
481
482DnutCompileOutput compileDnut(std::string_view source, const std::string& path,
483 const StepKindRegistry& registry) {
485 auto tokens = lexDnut(source, path);
486 if (!tokens.ok()) {
487 const auto* diagnostic = tokens.error();
488 output.diagnostics.push_back({DnutSeverity::Error, path, 0, 0,
489 diagnostic ? diagnostic->message() : "could not lex the document"});
490 return output;
491 }
492 StoryParser parser(std::move(tokens).takeValue(), path, registry, output);
493 parser.run();
494 return output;
495}
496
497} // namespace eve::dnut
ActionParameterOperation operation
Value::Object payload
Duration start
std::string output
std::string label
std::vector< CompiledStatement > thenBody
eve::Value condition
std::string nodeId
std::vector< CompiledStatement > elseBody
Registry-driven compiler for the `.dnut` story dialect.
Shared lexer for every `.dnut` dialect.
std::string message
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
HexVec3 left
HexVec3 right
std::string text
TokenKind kind
size_t offset
std::array< float, 3 > position
std::weak_ptr< Run > run
Definition OnnxGpgpu.cpp:25
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
uint64_t token
const RoadNode * node
const SquirrelValueOptions & options
std::string body
const UnitySourceAsset & source
std::uint32_t depth
std::uint64_t statements
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
bool isInt64() const noexcept
Return true when this value is an Int64.
Definition Value.h:91
std::map< std::string, Value > Object
Definition Value.h:34
double asDouble() const
Return the Double payload; the caller must have checked the kind.
Definition Value.cpp:75
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::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Definition Value.cpp:69
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
static Value boolean(bool value)
Compatibility factory for a boolean value.
Definition Value.h:108
Canonical step vocabulary owner and runtime handler router.
EVENGINE_API_PLATFORM bool hasErrors(const std::vector< DnutDiagnostic > &diagnostics)
Return whether any diagnostic is an error.
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
DnutCompileOutput compileDnut(std::string_view source, const std::string &path, const StepKindRegistry &registry)
Compile every story block of one .dnut document.
@ 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.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
const EditorValue * field(const EditorValue &value, const char *name)
Owning outcome of compiling one .dnut document.
std::vector< DnutDiagnostic > diagnostics
bool hasErrors() const
Return whether any collected diagnostic has error severity.
bool consumed
Definition Graphics.cpp:184