载入中...
搜索中...
未找到
DnutParser.cpp
浏览该文件的文档.
2
7
8#include <cstdio>
9#include <cstdlib>
10#include <stdexcept>
11#include <string>
12#include <utility>
13#include <vector>
14
15namespace eve::dialogue {
16namespace {
17
18class PoolParseError final : public std::runtime_error {
19public:
20 PoolParseError(const eve::dnut::DnutToken& token, std::string message)
21 : std::runtime_error(std::move(message)), line(token.line), column(token.column) {}
22
23 int line;
24 int column;
25};
26
27std::string floatToString(double value) {
28 char buffer[64];
29 std::snprintf(buffer, sizeof(buffer), "%g", value);
30 return buffer;
31}
32
33std::string scalarToString(const DataValue& value) {
34 if (value.isString()) return value.asString();
35 if (value.isInt64()) return std::to_string(value.asInt());
36 if (value.isDouble()) return floatToString(value.asDouble());
37 if (value.isBool()) return value.asBool() ? "true" : "false";
38 return {};
39}
40
41DataValue numberValue(const std::string& raw) {
42 if (raw.find_first_of(".eE") != std::string::npos)
43 return DataValue::number(std::strtod(raw.c_str(), nullptr));
44 return DataValue::integer(std::strtoll(raw.c_str(), nullptr, 10));
45}
46
47class PoolBlockParser {
48public:
49 PoolBlockParser(const std::vector<eve::dnut::DnutToken>& tokens, const eve::dnut::DnutBlock& block)
50 : tokens_(tokens), end_(block.endToken), index_(block.beginToken) {}
51
52 std::pair<std::string, DataValue> parse() {
53 expectKeyword("pool");
54 const std::string poolId = expectIdentifier("pool name");
55 long long noRepeat = -1;
56 while (isIdentifier("noRepeat")) {
57 advance();
58 expectPunct("=");
59 if (cur().kind != eve::dnut::DnutTokenKind::Number) fail("noRepeat must be a number");
60 noRepeat = std::strtoll(advance().text.c_str(), nullptr, 10);
61 }
62 expectPunct("{");
63
64 std::vector<DataValue> lines;
65 int lineIndex = 1;
66 while (!isPunct("}")) {
67 if (atEnd()) fail("unterminated pool block");
68 if (isIdentifier("pool")) fail("pool blocks cannot be nested");
69 if (isIdentifier("when")) {
70 advance();
71 DataValue condition = parseOr();
72 expectPunct("{");
73 while (!isPunct("}")) {
74 if (atEnd()) fail("unterminated when block");
75 lines.push_back(parseLine(poolId, lineIndex++, &condition));
76 }
77 advance();
78 } else {
79 lines.push_back(parseLine(poolId, lineIndex++, nullptr));
80 }
81 }
82 advance();
84 fields.emplace("lines", DataValue::array(std::move(lines)));
85 if (noRepeat >= 0) fields.emplace("noRepeat", DataValue::integer(noRepeat));
86 return {poolId, DataValue::object(std::move(fields))};
87 }
88
89private:
90 const eve::dnut::DnutToken& cur() const {
91 const std::size_t position = index_ < tokens_.size() ? index_ : tokens_.size() - 1;
92 return tokens_[position];
93 }
94 bool atEnd() const { return index_ >= end_ || cur().kind == eve::dnut::DnutTokenKind::EndOfFile; }
95 eve::dnut::DnutToken advance() {
96 const auto token = cur();
97 if (!atEnd()) ++index_;
98 return token;
99 }
100 bool isPunct(const char* text) const {
101 return cur().kind == eve::dnut::DnutTokenKind::Punctuator && cur().text == text;
102 }
103 bool isIdentifier(const char* text) const {
104 return cur().kind == eve::dnut::DnutTokenKind::Identifier && cur().text == text;
105 }
106 [[noreturn]] void fail(std::string message) const { throw PoolParseError(cur(), std::move(message)); }
107 void expectKeyword(const char* text) {
108 if (!isIdentifier(text)) fail("expected '" + std::string(text) + "'");
109 advance();
110 }
111 void expectPunct(const char* text) {
112 if (!isPunct(text)) fail("expected '" + std::string(text) + "'");
113 advance();
114 }
115 std::string expectIdentifier(const std::string& description) {
116 if (cur().kind != eve::dnut::DnutTokenKind::Identifier) fail("expected " + description);
117 return advance().text;
118 }
119
120 DataValue parseLiteral() {
121 if (isPunct("-")) {
122 advance();
123 if (cur().kind != eve::dnut::DnutTokenKind::Number) fail("'-' must be followed by a number");
124 DataValue value = numberValue(advance().text);
125 return value.isInt64() ? DataValue::integer(-value.asInt()) : DataValue::number(-value.asDouble());
126 }
127 if (cur().kind == eve::dnut::DnutTokenKind::String) return DataValue::string(advance().text);
128 if (cur().kind == eve::dnut::DnutTokenKind::Number) return numberValue(advance().text);
129 if (isIdentifier("true")) {
130 advance();
131 return DataValue::boolean(true);
132 }
133 if (isIdentifier("false")) {
134 advance();
135 return DataValue::boolean(false);
136 }
137 fail("expected a string, number, true, or false");
138 }
139
140 DataValue parseComparison() {
141 const std::string variable = expectIdentifier("a condition variable");
142 if (cur().kind != eve::dnut::DnutTokenKind::Punctuator) fail("expected a comparison operator");
143 const std::string operation = advance().text;
144 std::string mapped;
145 if (operation == "==")
146 mapped = "eq";
147 else if (operation == "!=")
148 mapped = "ne";
149 else if (operation == ">")
150 mapped = "gt";
151 else if (operation == "<")
152 mapped = "lt";
153 else if (operation == ">=")
154 mapped = "ge";
155 else if (operation == "<=")
156 mapped = "le";
157 else
158 fail("unsupported comparison operator '" + operation + "'");
159 return DataValue::object(
160 {{"var", DataValue::string(variable)}, {"op", DataValue::string(mapped)}, {"value", parseLiteral()}});
161 }
162
163 DataValue parseNot() {
164 if (isPunct("!")) {
165 advance();
166 return DataValue::object({{"not", parseNot()}});
167 }
168 if (isPunct("(")) {
169 advance();
170 DataValue value = parseOr();
171 expectPunct(")");
172 return value;
173 }
174 return parseComparison();
175 }
176
177 DataValue parseAnd() {
178 DataValue left = parseNot();
179 while (isPunct("&&")) {
180 advance();
181 DataValue right = parseNot();
182 if (DataValue* all = left.find("all"); all && all->isArray())
183 all->pushBack(std::move(right));
184 else
185 left = DataValue::object({{"all", DataValue::array({std::move(left), std::move(right)})}});
186 }
187 return left;
188 }
189
190 DataValue parseOr() {
191 DataValue left = parseAnd();
192 while (isPunct("||")) {
193 advance();
194 DataValue right = parseAnd();
195 if (DataValue* any = left.find("any"); any && any->isArray())
196 any->pushBack(std::move(right));
197 else
198 left = DataValue::object({{"any", DataValue::array({std::move(left), std::move(right)})}});
199 }
200 return left;
201 }
202
203 void parseAttributes(int sourceLine, DataValue::Object& out) {
204 while (!atEnd() && cur().line == sourceLine && !isPunct("}")) {
205 const std::string name = expectIdentifier("an attribute name");
206 if (name == "meta" && isPunct("(")) {
207 advance();
208 DataValue::Object metadata;
209 while (!isPunct(")")) {
210 const std::string key = expectIdentifier("a metadata key");
211 expectPunct("=");
212 DataValue value = parseLiteral();
213 const std::string scalar = scalarToString(value);
214 if (scalar.empty() && !value.isString()) fail("metadata values must be scalar");
215 metadata.emplace(key, DataValue::string(scalar));
216 if (isPunct(","))
217 advance();
218 else if (!isPunct(")"))
219 fail("expected ',' or ')' in metadata");
220 }
221 advance();
222 out.emplace("meta", DataValue::object(std::move(metadata)));
223 continue;
224 }
225 if (name == "tags") {
226 expectPunct("=");
227 expectPunct("[");
228 std::vector<DataValue> tags;
229 while (!isPunct("]")) {
230 if (cur().kind != eve::dnut::DnutTokenKind::String) fail("tag values must be quoted strings");
231 tags.push_back(DataValue::string(advance().text));
232 if (isPunct(","))
233 advance();
234 else if (!isPunct("]"))
235 fail("expected ',' or ']' in tags");
236 }
237 advance();
238 out.emplace("tags", DataValue::array(std::move(tags)));
239 continue;
240 }
241 expectPunct("=");
242 if (name != "weight" && name != "i18n" && name != "id") fail("unknown pool line attribute '" + name + "'");
243 out.emplace(name, parseLiteral());
244 }
245 }
246
247 DataValue parseLine(const std::string& poolId, int lineIndex, const DataValue* inheritedCondition) {
248 const int sourceLine = cur().line;
249 std::string speaker;
250 if (isPunct("-")) {
251 advance();
252 } else {
253 if (isIdentifier("when")) fail("when blocks cannot be nested");
254 speaker = expectIdentifier("a speaker or '-'");
255 expectPunct(":");
256 }
257 if (cur().kind != eve::dnut::DnutTokenKind::String) fail("expected quoted dialogue text");
259 fields.emplace("speaker", DataValue::string(speaker));
260 fields.emplace("text", DataValue::string(advance().text));
261 if (inheritedCondition) fields.emplace("when", *inheritedCondition);
262
263 DataValue::Object attributes;
264 parseAttributes(sourceLine, attributes);
265 bool hasId = false;
266 for (auto& [key, value] : attributes) {
267 if (key == "id") hasId = true;
268 fields.emplace(std::move(key), std::move(value));
269 }
270 if (!hasId) fields.emplace("id", DataValue::string(poolId + "." + std::to_string(lineIndex)));
271 return DataValue::object(std::move(fields));
272 }
273
274 const std::vector<eve::dnut::DnutToken>& tokens_;
275 std::size_t end_;
276 std::size_t index_;
277};
278
279void addError(std::vector<ConversationDiagnostic>& diagnostics, const std::string& path, int line, int column,
280 std::string message, std::string code = "DnutParseError") {
281 diagnostics.push_back({ConversationDiagnostic::Severity::Error,
282 path,
283 line,
284 path + ":" + std::to_string(line) + ": " + message,
285 std::move(code),
286 column,
287 {}});
288}
289
290bool validateEnvelope(const std::vector<eve::dnut::DnutToken>& tokens, const std::string& path,
291 std::vector<ConversationDiagnostic>& diagnostics, DnutDocument& document) {
292 if (tokens.size() < 5 || tokens[0].kind != eve::dnut::DnutTokenKind::Identifier || tokens[0].text != "schema" ||
294 tokens[2].text != "version" || tokens[3].kind != eve::dnut::DnutTokenKind::Number) {
295 const auto& token = tokens.front();
296 addError(diagnostics, path, token.line, token.column, "expected schema \"eve.dnut\" and version 1");
297 return false;
298 }
299 document.schema = tokens[1].text;
300 document.version = static_cast<int>(std::strtol(tokens[3].text.c_str(), nullptr, 10));
301 if (document.schema != "eve.dnut" || document.version != DnutDocument::CurrentVersion) {
302 addError(diagnostics, path, tokens[0].line, tokens[0].column, "unsupported dnut schema or version",
303 "UnsupportedSchemaVersion");
304 return false;
305 }
306 return true;
307}
308
309} // namespace
310
311eve::Result<DnutDocument> parseDnutDocument(const std::string& source, const std::string& path,
312 std::vector<ConversationDiagnostic>& diagnostics) {
313 DnutDocument document;
314 auto tokensResult = eve::dnut::lexDnut(source, path);
315 if (!tokensResult.ok()) {
316 const eve::Diagnostic* error = tokensResult.error();
317 int line = 1;
318 int column = 1;
319 if (error)
320 for (const auto& [key, value] : error->details()) {
321 if (key == "line") line = std::atoi(value.c_str());
322 if (key == "column") column = std::atoi(value.c_str());
323 }
324 addError(diagnostics, path, line, column, error ? error->message() : "could not lex dnut");
326 eve::DiagnosticCode::ParseError, diagnostics.back().message, path, {}, "dialogue.dnut.parse"));
327 }
328 std::vector<eve::dnut::DnutToken> tokens = std::move(tokensResult).takeValue();
329 if (!validateEnvelope(tokens, path, diagnostics, document))
331 eve::DiagnosticCode::UnknownVersion, diagnostics.back().message, path, {}, "dialogue.dnut.parse"));
332
333 auto blocksResult = eve::dnut::scanDnutBlocks(tokens, path);
334 if (!blocksResult.ok()) {
335 const eve::Diagnostic* error = blocksResult.error();
336 addError(diagnostics, path, 1, 1, error ? error->message() : "could not scan dnut blocks");
338 eve::DiagnosticCode::ParseError, diagnostics.back().message, path, {}, "dialogue.dnut.parse"));
339 }
340
341 DataValue::Object pools;
342 try {
343 for (const auto& block : blocksResult.value()) {
344 if (block.kind == "pool") {
345 auto [id, pool] = PoolBlockParser(tokens, block).parse();
346 if (pools.contains(id))
347 throw PoolParseError(tokens[block.beginToken], "duplicate pool id '" + id + "'");
348 pools.emplace(std::move(id), std::move(pool));
349 } else if (block.kind != "conversation" && block.kind != "story") {
350 throw PoolParseError(tokens[block.beginToken], "unknown top-level block '" + block.kind + "'");
351 }
352 }
353 } catch (const PoolParseError& error) {
354 addError(diagnostics, path, error.line, error.column, error.what());
356 eve::DiagnosticCode::ParseError, diagnostics.back().message, path, {}, "dialogue.dnut.parse"));
357 }
358 document.poolRoot = DataValue::object({{"pools", DataValue::object(std::move(pools))}});
359
361 auto registered = registerDialogueSequenceSteps(registry);
362 if (!registered.ok()) return eve::Result<DnutDocument>::failure(registered.status());
364 for (const auto& diagnostic : compiled.diagnostics)
365 diagnostics.push_back({diagnostic.severity == eve::dnut::DnutSeverity::Error
368 diagnostic.path,
369 diagnostic.line,
370 diagnostic.message,
371 "DnutParseError",
372 diagnostic.column,
373 {}});
374 if (compiled.hasErrors())
376 eve::DiagnosticCode::ParseError, diagnostics.back().message, path, {}, "dialogue.dnut.parse"));
377 document.conversations = std::move(compiled.assets);
378 return eve::Result<DnutDocument>::success(std::move(document));
379}
380
381} // namespace eve::dialogue
ActionParameterOperation operation
double value
Dialogue payload codecs for L1 sequence assets.
Shared top-level block discovery for `.dnut` dialects.
eve::Value condition
Registry-driven compiler for the `.dnut` story dialect.
Shared lexer for every `.dnut` dialect.
int column
std::string message
DiagnosticCode code
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::uint32_t key
HexVec3 left
HexVec3 right
std::string text
TokenKind kind
std::array< float, 3 > position
std::string description
std::string name
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::string id
Definition PlayHost.cpp:108
uint64_t token
double number
std::vector< std::string_view > lines
const UnitySourceAsset & source
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
std::map< std::string, Value > Object
Definition Value.h:34
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
static Value array(Array value)
Compatibility factory for an array value.
Definition Value.h:112
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
static Value object(Object value)
Compatibility factory for an object value.
Definition Value.h:114
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< DnutDocument > parseDnutDocument(const std::string &source, const std::string &path, std::vector< ConversationDiagnostic > &diagnostics)
Parse pools and conversations from one versioned dnut token stream.
eve::Value DataValue
Canonical data value used by dialogue pools, conditions and scripts.
Definition Dialogue.h:36
eve::Result< void > registerDialogueSequenceSteps(eve::dnut::StepKindRegistry &registry)
Register dialogue's line, choice, and command contracts and validators.
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
@ 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.
ModelData parse(std::span< const uint8_t > bytes)
Parse.
UI-neutral syntax tree produced by the single dnut parser.
static constexpr int CurrentVersion
One top-level dialect block in a shared token buffer.
Owning outcome of compiling one .dnut document.
std::vector< DnutDiagnostic > diagnostics
One lexed token with its exact source anchor.
Definition DnutLexer.h:31