Table of Contents

Progression Order

Note

Status: partially implemented. A script plays in reading order, a => jump never returns, and #END stops a run early; the dialogue graph builds these edges and the runner walks them. The returning detour and a #START entry are not built.

Table of contents

Goal and scope

A script needs a default flow: what plays after a line, and what happens when a scene's content runs out. That single choice decides everything downstream — most importantly, whether a jump returns. This note answers it for DialogueDown.

The model: reading order. DialogueDown progresses like a document you read — top to bottom, in document order. When a scene's content is exhausted, the reader falls through to the next block in the source, exactly as prose flows into the next paragraph and a chapter flows into the next chapter. A script with no jumps plays start to finish with no wiring.

In scope:

  • The reading-order progression model and its document-order semantics.
  • The two jump roles it creates: a non-returning divert (the existing =>) and a returning detour (concept only here).
  • The #END reserved terminator that stops a run early.
  • #END resolution, diagnostics, and editor support (highlighting and completion).

Out of scope / deferred (see open questions):

  • The detour's syntax and return boundary — its concrete spelling and where it returns get their own follow-up note; here it is only a named role.
  • #START / entry point — where a run begins (file top vs. a designated start vs. a cross-file entry) is deliberately left open.
  • Cross-file jumps.

Ubiquitous language

Term Meaning
Progression order The order a reader moves through a script when nothing branches. DialogueDown's is reading order.
Reading order Document order — the source top to bottom, which is the pre-order of the heading outline.
Fall-through When a scene's content is exhausted, the reader continues to the next block in document order rather than stopping.
Run One traversal of the script, from the entry point until it terminates.
Divert A non-returning jump: control transfers to the target and does not come back. The existing =>.
Detour A returning jump: control transfers to the target, plays it to depletion, then returns and continues reading order. Concept only in this note.
End sentinel The terminal node of a run. The reserved anchor #END resolves to it; reaching it ends the run.

One vocabulary applies across code, tests, docs, diagnostics, and commits.

Prior art

Every narrative language answers this same question, and they cluster into two camps. The lasting lesson: mature reading-order languages make the default jump non-returning and offer a separate returning construct, plus an explicit terminator.

Language Progression when a scene ends Non-returning Returning Explicit end
Ink Reading-order fall-through to the next knot -> divert ->-> tunnel -> END
ChoiceScript Reading-order; scenes chain in *scene_list order *goto *gosub / *return *finish
Ren'Py Reading-order; labels are bookmarks jump call / return return (top)
Yarn Spinner Section-terminal — a node ends, nothing follows <<jump>> <<detour>> (implicit)
Twine Section-terminal — a passage ends link (manual) (no link)

DialogueDown adopts the reading-order camp (Ink / ChoiceScript / Ren'Py). It is the least-surprising model for a script that looks like a Markdown document: a document is meant to be read straight through.

Writer-facing behavior

A script plays in document order. Headings are an outline; their nesting is for naming and scope, not flow — the reader simply reads the source top to bottom.

# The Crossroads
Guide: The road behind you is closed.

## The Signpost
Guide: A weathered signpost marks three roads.

# The Market
Merchant: Fresh apples!

This plays: The Crossroads → The Signpost (its subsection) → The Market — the document's own order, with no jumps required.

Diverting (=>, non-returning). A divert sends the reader elsewhere and does not come back:

Guide: Which way?
=> [To the market](#the-market)
Guide: (never reached — the divert already left)

Ending a run early (#END). Because scenes fall through, a branch that should stop — a bad ending in the middle of the document — needs an explicit terminator. Divert to the reserved anchor #END:

# Poisoned
Guard: You drank it. You collapse.
=> [The end](#END)

# The Vault
... (reached only by an explicit jump, never by falling through Poisoned)

Reaching the end of the last block also terminates naturally, so #END is only for stopping early. There is no # End heading to write: #END is always available. Any farewell "ceremony" is just ordinary content written before the terminator.

Editor sentinel row. The Source editor keeps language-owned targets visible in a fixed, read-only panel below the scrollable document. The End sentinel appears as:

∞   End                                              #END

Clicking it copies [End](#END), matching a heading's paste-ready jump-link affordance. The infinity glyph is a sentinel marker, not a line number: the row does not change source text, line counts, selection, search, folding, diagnostics, or undo history.

The report model names this concept a reserved target with one of two roles: Entry or Terminal. Only #END (Terminal) exists. A #START (Entry) would project through the same metadata and panel without turning either sentinel into a source heading.

Choices rejoin. After the reader picks an option, that option's body plays, then control continues after the choice block — the branch weaves back into the main flow. The same holds for a random choice once its option is resolved.

Detour (returning) — the concept. A returning detour goes to a target, plays it to depletion, then returns and continues reading order — "expand this section inline, then carry on." It is what makes multiple jumps on one line meaningful (do this, come back, then do that, come back). Its concrete syntax and return boundary are a separate note; here it exists only as a named role that the diagnostics below already account for.

Grammar and semantics

Progression = document order. The flow graph's default succession edge connects each block to the next block in source order (the pre-order of the SceneHeading outline). The scene tree from the Semantic Analyzer is a naming/scope view; it does not change progression.

Divert is non-returning. A => transfers control to its target with no return edge. Anything after a divert on the same run path is unreachable (see diagnostics).

#END is an uppercase, case-sensitive reserved anchor. It resolves to the End sentinel — the run's terminal node — and is recognized before ordinary slug lookup. Two facts make the uppercase spelling collision-free by construction:

  • Heading slugs are always lowercased (Slug.From → ToLowerInvariant), so no heading can ever produce the slug END.
  • Jump targets are matched verbatim, so a lowercase user anchor (#end, from a scene titled "End") and the reserved #END never coincide.

So #END needs no new syntax — it is an ordinary divert to a reserved target — yet it can never clash with an author's scene. There is no injected heading: #END resolves straight to the sentinel, keeping the Markdown and Dialogue ASTs faithful to the source. The flow graph still renders an End node (the sentinel), so the terminus stays visible in the visualization.

flowchart LR
    A["Scene A"] -->|succession| B["Scene B"]
    B -->|"=> divert (non-returning)"| D["Scene D"]
    B -.->|content after a divert| U(["unreachable"])
    C["Poisoned"] -->|"=> [The end](#END)"| END(["End sentinel"])

Detour is returning (not built): a detour would add a return edge, so control resumes after it once the target depletes. This is the only shape under which trailing content or a second jump on a line is reachable.

Affected stages and seams

Stage Change
Semantic Analyzer — jump resolution Recognize the reserved #END anchor before AnchorTable lookup and resolve it to the End sentinel (TerminalJump); ordinary anchors resolve by slug.
Validation An unreachable-after-a-jump rule (DLG1003) that also covers a second jump on a line (see diagnostics).
Semantic model Expose the End sentinel and reserved-anchor resolution so the graph builder and editor projections can consume them.
Editor projections (visualization) Surface #END through the semantic symbol projection so completion offers it as a divert target; project it as a typed reserved target for the fixed Source-editor panel; and add a semantic token so source occurrences highlight as a reserved keyword. See the Compiler-Projected Editor Semantics note.
Dialogue graph and runner Succession and divert edges and an end node; the runner follows a divert before a succession. No detour edge exists.

Markdown interaction

#END introduces no new sigil — it is the anchor part of an ordinary divert link, so it consumes no literal character sequence and needs no new escape. In a plain Markdown preview, => [The end](#END) renders as a link whose fragment (#END, uppercase) matches no lowercased heading id, so it simply scrolls nowhere — acceptable for a control keyword the compiler interprets specially.

Diagnostics

Reading order plus a non-returning jump makes one structural check meaningful. It is a warning (dead content, not malformed input).

  • Unreachable content after a divert (DLG1003). In a line's speech, any non-blank fragment after the first divert — trailing text, or a second => — can never play, because the divert already left. Warn, spanning the unreachable content.
  • A second jump on a line is unreachable content after the first, so the same rule covers it. A returning detour would chain legitimately, so the rule keys off the non-returning jump.

A divert to an unknown reserved anchor (for example a mistyped #ENND) is an unresolved target, reported like any other missing anchor.

Testability

  • Pure and structural. #END resolution is a semantic-model unit test (a divert to #END resolves to the End sentinel, before slug lookup, case-sensitively). The diagnostics are structural rule tests over Line speech — the same shape as the existing jump rules — needing no runtime.
  • Editor. Component tests assert #END is offered by completion and carries its semantic token; a thin browser test proves the integration.
  • Play time. Fall-through, divert, and termination are covered by the conformance corpus (linear-speech, a-jump) and the runner's tests.

Alternatives not chosen

  • Section-terminal progression (Yarn / Twine). Every scene ends unless it explicitly moves on. Rejected: a straight-through Markdown read would need a jump at the end of every scene — verbose and unlike reading a document.
  • A returning default =>. Making the default jump deplete-and-return (so multiple jumps always chain) was considered. Rejected: no mainstream language does it — a one-way jump is the common branching case, so returning is opt-in everywhere. DialogueDown keeps => non-returning and adds the detour separately.
  • Lowercase #end with an override heading. Reserving the lowercase slug end (letting an authored # End scene be the terminator, with ceremony) was the first sketch. Rejected for the uppercase #END sentinel: it collides with a real # End scene and needs an override rule and an INFO diagnostic, where #END collides with nothing and stays a pure sentinel.
  • ##end reserved-tag terminator. Spelling the terminator in the ## reserved namespace avoids the slug question but overloads reserved tags (a speaker/line annotation) with control flow and breaks the uniform "a jump targets a link" grammar.

Open questions and deferred work

  • Detour syntax and return boundary — the returning construct's spelling and where it returns (a heading's subtree is the leading candidate) get their own note.
  • #START / entry point — reserving a start sentinel is natural, but its meaning (file top vs. a designated start vs. cross-file entry) is unsettled and deferred.
  • Case-insensitive scene-target matching — ordinary jump targets are matched case-sensitively against lowercased slugs, so a hand-typed #The-Market fails to resolve. Autocomplete inserts the correct slug, so it rarely bites, but it is a latent sharp edge worth its own fix.