9#include <unordered_set>
16class ConversationParseError final :
public std::runtime_error {
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));
30class ConversationBlockParser {
32 ConversationBlockParser(
const std::vector<DnutToken>&
tokens,
const DnutBlock& block)
33 : tokens_(
tokens), end_(block.endToken), index_(block.beginToken) {}
35 SequenceAsset
parse() {
36 const DnutToken declaration = cur();
37 expectKeyword(
"conversation");
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");
46 asset.entry = scalarText();
47 else if (
key ==
"version") {
49 asset.version =
static_cast<int>(std::strtol(advance().
text.c_str(),
nullptr, 10));
51 fail(
"unknown conversation field '" +
key +
"'");
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());
62 fail(
"conversation accepts only parameter or node declarations");
69 const DnutToken& cur()
const {
70 const std::size_t
position = index_ < tokens_.size() ? index_ : tokens_.size() - 1;
75 const DnutToken
token = cur();
76 if (!atEnd()) ++index_;
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) +
"'");
86 void expectKeyword(
const char*
text) {
87 if (!isIdentifier(
text)) fail(
"expected '" + std::string(
text) +
"'");
90 std::string expectIdentifier(
const std::string&
description) {
92 return advance().text;
94 std::string scalarText() {
97 fail(
"expected a scalar value");
98 return advance().text;
111 if (isIdentifier(
"true")) {
115 if (isIdentifier(
"false")) {
121 fail(
"expected a literal value");
127 while (!isPunct(
")")) {
128 const std::string
key = expectIdentifier(
"an argument name");
133 else if (!isPunct(
")"))
134 fail(
"expected ',' or ')' in argument list");
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")
149 else if (
type ==
"int")
151 else if (
type ==
"float")
153 else if (
type ==
"bool")
156 fail(
"unknown parameter type '" +
type +
"'");
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") {
166 parameter.defaultValue = parseLiteral();
167 parameter.required =
false;
169 fail(
"unknown parameter field '" + field +
"'");
181 mutations->
pushBack(std::move(mutation));
184 SequenceRoute parseRoute() {
185 const DnutToken declaration = advance();
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());
197 if (
key ==
"mutation") {
198 appendMutation(route.payload, parseObjectArguments());
203 if (
key ==
"target") {
204 if (!
value.isString()) fail(
"route target must be a node id");
205 route.target =
value.asString();
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));
212 fail(
"unknown route field '" +
key +
"'");
215 if (!hasTarget) fail(
"route requires target");
219 SequenceNode parseNode() {
220 const DnutToken declaration = advance();
222 node.sourceLine = declaration.line;
223 node.sourceColumn = declaration.column;
224 node.id = expectIdentifier(
"a node id");
225 node.type = expectIdentifier(
"a node type");
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());
233 if (
key ==
"payment") {
234 node.payload.set(
"payment", parseObjectArguments());
237 if (
key ==
"mutation") {
238 appendMutation(
node.payload, parseObjectArguments());
244 if (!
value.isString()) fail(
"next must be a node id");
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));
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());
266 const std::vector<DnutToken>& tokens_;
272 const char* defaultMessage) {
276 for (
const auto& [
key,
value] : diagnostic->details()) {
281 output.diagnostics.push_back(
285bool validateDocumentEnvelope(
const std::vector<DnutToken>&
tokens,
const std::string&
path,
286 DnutCompileOutput&
output) {
292 output.diagnostics.push_back(
305 if (!tokensResult.ok()) {
306 appendResultFailure(
output,
path, tokensResult.error(),
"could not lex the document");
309 std::vector<DnutToken>
tokens = std::move(tokensResult).takeValue();
313 if (!blocksResult.ok()) {
314 appendResultFailure(
output,
path, blocksResult.error(),
"could not scan the document");
318 std::unordered_set<std::string> assetIds;
319 for (
const auto& block : blocksResult.value()) {
320 if (block.kind !=
"conversation")
continue;
324 if (!assetIds.insert(asset.
id).second) {
326 "duplicate conversation id '" + asset.
id +
"'"});
331 graph.error() ?
graph.error()->message() :
"invalid conversation graph"});
337 if (!validated.ok()) {
340 validated.error() ? validated.error()->message() :
"invalid conversation node"});
344 if (
valid)
output.assets.push_back(std::move(asset));
345 }
catch (
const ConversationParseError&
error) {
Shared top-level block discovery for `.dnut` dialects.
Registry-driven compiler for the `.dnut` story dialect.
Shared lexer for every `.dnut` dialect.
std::unordered_map< const Graphics *, std::shared_ptr< Lifetime > > tokens
std::array< float, 3 > position
std::map< std::string, std::vector< std::string > > graph
std::vector< std::string > fields
const UnitySourceAsset & source
A structured explanation of a failed, degraded, or noteworthy result.
const std::string & message() const noexcept
Human-readable explanation.
const std::vector< Diagnostic > & diagnostics() const noexcept
Inspect all diagnostics; this counts as checking the Result.
The canonical owning dynamic value used by data-facing protocols.
std::map< std::string, Value > Object
void set(const std::string &key, Value value)
Insert or replace an object member.
static Value number(double value)
Compatibility factory for a Double value.
static Value string(std::string value)
Compatibility factory for a string value.
std::vector< Value > Array
static Value integer(std::int64_t value)
Compatibility factory for an Int64 value.
void pushBack(Value value)
Append an element to an array value.
static Value boolean(bool value)
Compatibility factory for a boolean value.
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
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
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.
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 ®istry)
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).
Owning outcome of compiling one .dnut document.
One lexed token with its exact source anchor.
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