Skip to content

Render pipeline

render(source, options) never panics and never runs unbounded. Every step either checks a size limit or draws from one per-diagram fuel counter, and a failure comes back as a result with an error and diagnostics, never as an exception from the core.

One render with its limits Flowchart, left to right. 13 nodes, 14 edges. source check input ≤ 1 MiB → parse nesting ≤ 64, labels ≤ 4 KiB; → TooLarge E004, CLI exit 3 parse nesting ≤ 64, labels ≤ 4 KiB → size caps ≤ 2,000 nodes, ≤ 4,000 edges size caps ≤ 2,000 nodes, ≤ 4,000 edges → parse hint 1 optional unit per byte parse hint 1 optional unit per byte → measure labels 1 unit per byte measure labels 1 unit per byte → phases 1–2 cycles, layers ≤ 500 phases 1–2 cycles, layers ≤ 500 → layered graph nodes + dummies ≤ 20,000 layered graph nodes + dummies ≤ 20,000 → phase 3 sweeps mandatory; → TooLarge E004, CLI exit 3 phase 3 sweeps mandatory → phase 3 refinement exact + local objective; → TooLarge E004, CLI exit 3 phase 3 refinement exact + local objective → phases 4–7 coordinates, routing, labels phases 4–7 coordinates, routing, labels → container fit alternatives while fuel lasts container fit alternatives while fuel lasts → draw SVG + outline sourcecheck input ≤ 1 MiB parsenesting ≤ 64, labels ≤ 4 KiB size caps≤ 2,000 nodes, ≤ 4,000 edges parse hint1 optional unit per byte measure labels1 unit per byte phases 1–2cycles, layers ≤ 500 layered graphnodes + dummies ≤ 20,000 phase 3 sweepsmandatory phase 3 refinementexact + local objective phases 4–7coordinates, routing, labels container fitalternatives while fuel lasts drawSVG + outline TooLargeE004, CLI exit 3
One render with its limits
Diagram source
flowchart LR
accTitle: One render with its limits
src["**source**<br/>check input ≤ 1 MiB"] --> parse["**parse**<br/>nesting ≤ 64, labels ≤ 4 KiB"]
parse --> caps["**size caps**<br/>≤ 2,000 nodes, ≤ 4,000 edges"]
caps --> hint["**parse hint**<br/>1 optional unit per byte"]
hint --> measure["**measure labels**<br/>1 unit per byte"]
measure --> p12["**phases 1–2**<br/>cycles, layers ≤ 500"]
p12 --> build["**layered graph**<br/>nodes + dummies ≤ 20,000"]
build --> p3["**phase 3 sweeps**<br/>mandatory"]
p3 --> refine["**phase 3 refinement**<br/>exact + local objective"]
refine --> rest["**phases 4–7**<br/>coordinates, routing, labels"]
rest --> fit["**container fit**<br/>alternatives while fuel lasts"]
fit --> draw["**draw**<br/>SVG + outline"]
big["**TooLarge**<br/>E004, CLI exit 3"]
src e1@--> big
build e2@--> big
p3 e3@--> big
class src input
class p12,p3 accent
class refine,fit,hint optional
class draw output
class big danger
class e1,e2,e3 failure

Dashed boxes are optional work: when fuel runs short they stop and keep the best result so far. Every other step is mandatory; a limit or an empty fuel counter there ends the render with TooLarge. The failure edges show three representative exits; every mandatory step has one.

Limit Default Exceeded
Input size 1 MiB TooLarge { what: "input" }
Declared nodes / edges 2,000 / 4,000 TooLarge { what: "nodes" } / "edges"
Layers 500 TooLarge { what: "layers" }
Layered graph: nodes plus dummy nodes 20,000 TooLarge { what: "layered nodes" }
Subgraph / front matter / %%{init}%% nesting 64 E010 / E011 / E012 error
Label length 4,096 bytes Truncated at a character boundary, W012
Fuel 20,000,000 units Optional passes stop; a mandatory phase returns TooLarge { what: "fuel" }

Every limit is a render option (Limits in crates/merlion-render/src/options.rs). The dummy-node cap exists because an edge spanning L layers adds L − 1 dummies: 2,000 nodes with 2,000 long edges would otherwise reach 4 million.

One fuel unit is one step of an inner loop: one node visited in cycle removal, one neighbour scanned in a sweep, one search node in exact refinement, one byte of label text measured. The counter lives in crates/merlion-render/src/fuel.rs and has two ways to spend:

  • burn — mandatory work. It may use every unit up to the limit; running out is TooLarge.
  • burn_optional — optional work. It fails, without spending, once only the reserve is left.

Before phase 3, the layout computes the reserve from the layered graph’s size (64 units per node, segment and chain, plus 4,096) and holds it back for coordinates, routing and labels. So an optional pass can never starve a mandatory one, and a diagram that fits its budget always renders.

How a fuel request is decided Flowchart, left to right. 6 nodes, 6 edges. request n units → used + n ≤ limit? [mandatory]; → used + n ≤ limit − reserve? [optional] used + n ≤ limit? → spend used += n [yes]; → TooLarge what: fuel [no] used + n ≤ limit − reserve? → spend used += n [yes]; → pass stops best result kept [no] mandatory optional yes yes no no requestn units used + n ≤ limit? used + n ≤ limit − reserve? spendused += n TooLargewhat: fuel pass stopsbest result kept
How a fuel request is decided
Diagram source
flowchart LR
accTitle: How a fuel request is decided
req{"**request**<br/>n units"} -->|mandatory| m{"used + n ≤ limit?"}
req -->|optional| o{"used + n ≤ limit − reserve?"}
m -->|yes| spend["**spend**<br/>used += n"]
o -->|yes| spend
m e1@-->|no| tl["**TooLarge**<br/>what: fuel"]
o -->|no| stop["**pass stops**<br/>best result kept"]
class req,m,o warn
class spend accent
class tl danger
class stop muted
class e1 failure

Fuel replaces a time budget because a clock would make output depend on machine load; with fuel, the same input and options render the same bytes on a loaded CI runner and on a laptop (ADR-0008). Container fit spends fuel the same way: it tries another alternative only while the fuel left covers one more candidate the size of the first drawing (Container fit).

The CLI’s --json and the WASM module emit the same JSON, written by crates/merlion-render/src/json.rs:

{ "svg": "<svg …>", "outline": "Flowchart, left to right. …", "diagnostics": [], "fuel_used": 18342, "error": null }
error Meaning Diagnostic
null Rendered; svg and outline are set Warnings, repairs and infos only
{ "kind": "parse" } The source failed to parse E002 or the parser’s own error
{ "kind": "unsupported_diagram", "header": "pie" } Not a flowchart E003
{ "kind": "too_large", "what": "layers" } A limit or mandatory fuel ran out E004
{ "kind": "internal" } WASM only: the module trapped; the glue replaces the instance E001

The JavaScript glue converts names to camelCase (fuelUsed, byteStart). The CLI exits 0 when every diagram rendered, 1 when one failed, 2 on a usage error and 3 when an input exceeded a limit. How to read and repair diagnostics: Diagnostics and repairs.