7#include <unordered_set>
22struct CompiledStatement {
23 enum class Kind { Step, If, Choice };
37struct CompiledOption {
40 std::vector<CompiledStatement>
body;
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));
49bool isComparisonOperator(
const std::string&
text) {
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";
64 StoryParser(std::vector<DnutToken>
tokens, std::string
path,
const StepKindRegistry& registry,
70 if (isIdentifier(
"story")) {
73 }
catch (
const ParseAbort&) {
83 const DnutToken& cur()
const {
return tokens_[index_]; }
84 const DnutToken& peek(std::size_t
offset)
const {
91 const DnutToken
token = tokens_[index_];
92 if (!atEnd()) ++index_;
95 bool isPunct(
const std::string&
text)
const {
98 bool isIdentifier(
const std::string&
text)
const {
103 output_.diagnostics.push_back(
105 errorLine_ =
token.line;
110 void expectPunct(
const std::string&
text) {
111 if (!isPunct(
text)) errorHere(
"expected '" +
text +
"'");
114 std::string expectIdentifier(
const std::string& what) {
116 return advance().text;
118 std::string nextNodeId() {
return "n" + std::to_string(++nodeCounter_); }
126 if (isPunct(
"}"))
return;
131 }
else if (isPunct(
"}")) {
132 if (
depth == 0)
return;
141 void skipForeignBlock() {
144 if (
depth == 0 && isIdentifier(
"story"))
return;
147 }
else if (isPunct(
"}")) {
156 invalidStep_ =
false;
157 const std::string
id = expectIdentifier(
"a story id");
158 bool repeatable =
false;
161 const std::string modifier = cur().text;
162 if (modifier ==
"repeatable") {
167 if (modifier ==
"version") {
171 version =
static_cast<int>(std::strtol(advance().
text.c_str(),
nullptr, 10));
172 if (version < 1) errorHere(
"version must be positive");
175 errorHere(
"unknown story modifier '" + modifier +
"'");
178 std::vector<CompiledStatement>
body;
179 parseBlockBody(
body);
183 asset.version = version;
184 asset.repeatable = repeatable;
185 const std::string endId = nextNodeId();
186 SequenceNode endNode;
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));
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"});
200 if (!publishedIds_.insert(asset.id).second) {
201 output_.diagnostics.push_back(
205 if (publishable) output_.assets.push_back(std::move(asset));
208 void parseBlockBody(std::vector<CompiledStatement>& out) {
211 if (atEnd()) errorHere(
"unterminated block");
217 out.push_back(parseStatement());
218 }
catch (
const ParseAbort&) {
224 CompiledStatement parseStatement() {
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") {
233 CompiledStatement statement;
234 statement.type =
"end";
235 statement.nodeId = nextNodeId();
241 CompiledStatement parseStep() {
242 const DnutToken
start = cur();
243 CompiledStatement statement;
244 statement.type = advance().text;
245 statement.nodeId = nextNodeId();
247 peek(1).
text ==
"=") {
248 const std::string
field = advance().text;
250 statement.payload.set(field, parseValue());
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();
263 diagnostic ? diagnostic->message() :
"invalid step"});
269 CompiledStatement parseWait() {
271 CompiledStatement statement;
272 statement.type =
"wait";
273 statement.nodeId = nextNodeId();
275 statement.payload.set(
"duration", numberValue(advance().
text));
278 if (isIdentifier(
"duration")) {
281 statement.payload.set(
"duration", parseValue());
284 errorHere(
"wait requires a duration");
287 CompiledStatement parseCall() {
289 CompiledStatement statement;
290 statement.type =
"call";
291 statement.nodeId = nextNodeId();
292 statement.payload.set(
"target",
eve::Value::string(expectIdentifier(
"a called story id")));
296 CompiledStatement parseIf() {
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")) {
305 parseBlockBody(statement.elseBody);
310 CompiledStatement parseChoice() {
312 CompiledStatement statement;
313 statement.kind = CompiledStatement::Kind::Choice;
314 statement.nodeId = nextNodeId();
316 while (!isPunct(
"}")) {
317 if (atEnd()) errorHere(
"unterminated choice block");
318 if (!isIdentifier(
"option")) errorHere(
"choice accepts only option statements");
320 CompiledOption option;
321 option.nodeId = nextNodeId();
323 option.label = advance().text;
324 if (isIdentifier(
"when")) {
326 option.condition = parseCondition();
328 parseBlockBody(option.body);
329 statement.options.push_back(std::move(option));
332 if (statement.options.empty()) errorHere(
"choice requires at least one option");
336 eve::Value parseCondition() {
return parseOr(); }
340 while (isPunct(
"||")) {
344 object !=
nullptr &&
object->contains(
"any") &&
object->at(
"any").isArray()) {
345 object->at(
"any").pushBack(std::move(
right));
356 while (isPunct(
"&&")) {
360 object !=
nullptr &&
object->contains(
"all") &&
object->at(
"all").isArray()) {
361 object->at(
"all").pushBack(std::move(
right));
381 return parseComparison();
386 const std::string variable = advance().text;
388 errorHere(
"condition requires a comparison operator (== != > < >= <=)");
389 const std::string
operation = advance().text;
393 {
"value", std::move(expected)}});
404 switch (cur().
kind) {
408 if (isIdentifier(
"true")) {
412 if (isIdentifier(
"false")) {
417 default: errorHere(
"expected a literal value");
break;
421 std::string emitBlock(
const std::vector<CompiledStatement>&
statements,
const std::string& next,
422 SequenceAsset& asset) {
423 std::string continuation =
next;
425 continuation = emitStatement(*it, continuation, asset);
429 std::string emitStatement(
const CompiledStatement& statement,
const std::string& next, SequenceAsset& asset) {
430 if (statement.kind == CompiledStatement::Kind::Step) {
432 node.id = statement.nodeId;
433 node.type = statement.type;
435 node.payload = statement.payload;
436 asset.nodes.push_back(std::move(
node));
437 return statement.nodeId;
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);
443 node.id = statement.nodeId;
444 node.type =
"branch";
445 node.next = elseEntry;
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;
454 node.id = statement.nodeId;
455 node.type =
"choice";
457 for (
const auto& option : statement.
options) {
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));
464 asset.nodes.push_back(std::move(
node));
465 return statement.nodeId;
468 std::vector<DnutToken> tokens_;
469 std::size_t index_ = 0;
471 const StepKindRegistry& registry_;
472 DnutCompileOutput& output_;
473 int nodeCounter_ = 0;
476 bool invalidStep_ =
false;
477 std::unordered_set<std::string> publishedIds_;
487 const auto* diagnostic =
tokens.error();
489 diagnostic ? diagnostic->message() :
"could not lex the document"});
ActionParameterOperation operation
std::vector< CompiledStatement > thenBody
std::vector< CompiledStatement > elseBody
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
const SquirrelValueOptions & options
const UnitySourceAsset & source
The canonical owning dynamic value used by data-facing protocols.
bool isInt64() const noexcept
Return true when this value is an Int64.
std::map< std::string, Value > Object
double asDouble() const
Return the Double payload; the caller must have checked the kind.
static Value number(double value)
Compatibility factory for a Double value.
static Value string(std::string value)
Compatibility factory for a string value.
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
std::vector< Value > Array
static Value integer(std::int64_t value)
Compatibility factory for an Int64 value.
static Value boolean(bool value)
Compatibility factory for a boolean value.
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.
DnutCompileOutput compileDnut(std::string_view source, const std::string &path, const StepKindRegistry ®istry)
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).
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.