Skip to content

3. Specification structure, layers and viewpoints

Contents of DISL 0.5

3.1 Top-level object

Property Type Req. Layer Description
$schema string – – URI of the DISL JSON Schema.
disl string ✓ – Targeted DISL version, "0.1" or "0.2" (2.9).
language Language ✓ – Identity, version, locales and limits (3.2).
imports Import[] – – Reused specifications and libraries (3.3).
functions map → Function – – Reusable CEL functions (3.4).
metamodel Metamodel ✓ 1 What can exist (section 4).
coordinates Coordinates – 2 Axes, coordinate systems, snapping (section 5). Default: one free cartesian pixel system without snapping.
notation Notation – 3 How things look (section 6). Default: every node a labelled rectangle, every relation a straight line with an arrow.
toolbox Toolbox – 4 Palette and context tools (7.1–7.4). Default: one tool per concrete type.
forms map → Form – 4 Property and embedded forms (7.5). Default: generated from attributes.
constraints Constraints – 5 Rules (section 8).
behavior Behavior – 6 Hooks, operations, deletion and clipboard policy (section 9).
layout Layout – 7 Automatic layout (section 10). Default: none.
persistence Persistence – 8 Storage (section 11). Default: single JSON file, UUIDv7 ids.
viewpoints map → Viewpoint – – Several diagram kinds over one model (3.5).
plugins map → Plugin – – Declared extensions (section 13).
doc Doc – – Documentation of the language as a whole.
x-* any – – Extensions.

Only disl, language and metamodel are required. Every other layer has a documented default, so specifications can start tiny and grow.

{
  "disl": "0.2",
  "language": { "id": "org.example.mindmap", "version": "0.1.0", "label": "Mind map" },
  "metamodel": {
    "types": { "Idea": { "attributes": { "text": { "type": "string", "required": true } } } },
    "relations": { "Branch": { "source": "Idea", "target": "Idea" } }
  }
}

This five-line language already yields a usable diagram type: a palette with an Idea tool and a Branch tool, rectangles labelled with text (the first string attribute is the default label), arrows, a generated property form, and JSON persistence.

3.2 Language

Property Type Req. Description
id qualified identifier ✓ Globally unique language id; reverse-DNS style is RECOMMENDED. Recorded in every DID definition.
version SemVer string ✓ Version of this language.
origin string <vendor>/<type> – The origin of the tool type this specification implements, matching ^[a-z0-9-]+/[a-z0-9-]+$ (for example "w3c/rdf", "wardley/map"). A document's registration names its tool type by origin, and FBL, the Format Binding Language, refers to it; a host maps an origin to the specification that implements it. Successive versions of one tool type keep the same origin. Supersedes the extension property x-adp-origin, which stays valid (2.8) but SHOULD NOT be written beside origin; when both are present, origin wins.
label LocalizedText – Display name ("State machine").
doc Doc – Language documentation, shown in the runtime's help view.
defaultLocale BCP 47 tag – Default "en".
locales string[] – Locales for which all LocalizedTexts SHOULD provide translations; validators MAY warn about gaps.
authors {name, email, url}[] – Maintainers (tool engineers).
license SPDX expression – License of the specification.
homepage URI – Project page.
icon IconRef – Icon of the language (file type icon, window title).
fileExtension string – Preferred extension of stored diagrams without dot, for example "sm.json".
limits object – celCost (per-expression cost limit), maxElements (soft limit, runtimes SHOULD warn beyond it), maxDocumentBytes, budgets (map id → Budget, hard budgets, 3.2.1).
requires object – conformance: minimum runtime conformance level ("core", "standard", "full"); features: list of optional feature ids (Appendix B.8) the specification relies on.

3.2.1 Budgets

A budget caps how much of a model a view draws, and says what the tool withholds while the cap is reached. language.limits.budgets maps a budget id (a simple identifier) to a Budget ($defs/Budget):

Property Type Default Description
measure { "count": TypeRef[] } or { "cel": … } required What is counted. count: the drawn candidates of these node and relation types after steps 1 to 3 of the drawing order (6.1). cel: an int in the budget context, for a measure that is not a count of drawn elements (for example a count of RDF nodes a declared function supplies).
max int ≥ 0 required The limit. The budget is exceeded while the measure is greater than max.
truncate bool true for count, false for cel Whether the budget cuts what is drawn. A budget that does not truncate only gates edits and shows its notice and finding.
order Expression persistence order Sort key per candidate, ascending, in the budget context; ties keep persistence order. It MAY return a list, compared element by element.
unit Expression each element its own unit A group key in the budget context: the candidates with one key are kept or cut together.
notice bool or {text, severity, position} true The built-in notice budget:<id> (6.13) shown while the budget truncates; a notice declared in canvas.notices with the id budget:<id> replaces it (6.13). text is a Message in the chrome context; its default names budget('<id>').shown and budget('<id>').total and says that edits are withheld when they are.
withhold { "edits": "model" | "all" | "none", "reason": Reason } ($defs/BudgetWithhold) { "edits": "model" } with the runtime's sentence What is refused while the budget is exceeded: model, every change to the model (property writes, operations with model actions, model-changing gestures, paste, delete); all, also stored view data such as positions; none, nothing.
finding {severity, message} none A diagram-level finding reported while the budget is exceeded; severity defaults to info, message is a Message in the constraint context. Because findings never depend on a viewer, the finding is judged on the measure taken without viewer filters (after steps 1 and 2 of 6.1).
doc Doc

The budget context binds self (the candidate), index (its position in persistence order), diagram and env (12.3). A budget's state is read in CEL with budget(id) (12.4), which returns shown, total, truncated and withheld; it is a function rather than a Diagram member so that diagram attributes named shown, total or truncated, which existing specifications declare, keep their meaning.

  • Budgets MUST be applied at step 4 of the drawing order (6.1), after viewer filters, and deterministically: the same model, viewer filters and specification MUST give the same kept set in every runtime.
  • For a truncating budget, candidates MUST be sorted by order; with unit, units are kept whole in the order of their first candidate while they fit, and the first unit that does not fit ends the cut. A truncating count budget MUST draw exactly the kept candidates. A relation MUST be drawn only when both its ends are kept, and a counted relation only when it is itself within the budget.
  • While a budget with withhold.edits other than none is exceeded, a runtime MUST refuse the withheld edits with withhold.reason, MUST show that reason as the read-only reason of every property row (4.3) and as the reason of every unavailable entry (9.3), and MUST NOT apply an edit partly. The withheld reason is checked with the edit gate (9.1), after a notation refusal and before every constraint (8.4).
  • A budget MUST NOT remove model data, or stored view data of the elements it leaves undrawn. Selecting, revealing or navigating to an undrawn element (for example from a finding) MAY be refused; the finding still names it (2.3).
  • limits.maxElements keeps its 0.1 meaning: a soft limit beyond which runtimes SHOULD warn. It is independent of budgets.
"limits": {
  "maxElements": 1000,
  "budgets": {
    "cards": {
      "measure": { "count": ["NodeShape"] }, "max": 1000,
      "notice": { "text": { "cel": "'Showing ' + string(budget('cards').shown) + ' of ' + string(budget('cards').total) + ' shapes.'" } },
      "withhold": { "edits": "none" } },
    "rdfNodes": {
      "measure": { "cel": "rdfNodeCount()" }, "max": 1000, "notice": false,
      "withhold": { "edits": "model", "reason": "The diagram shows only the first part of this file, so edits through it are withheld: an edit through a partial view could touch what the view does not show. Edit the file as text instead." } }
  }
}

The SHACL shapes graph above draws at most 1000 cards, in discovery order, and withholds edits by a second measure, the RDF nodes of the file. A budget over several types keeps them in a given order with a list key, for example "order": "[typeRank(self), index]" for a SPARQL query's regions, variables, pattern edges and annotations, and a budget that must keep whole groups declares unit, for example "unit": "unitOf(self)" for an OWL axiom and its expressions.

3.3 Imports

Specifications may import other specifications or libraries — specifications that contain only notation, forms, functions or data types and whose metamodel may be empty.

Property Type Req. Description
from URI or relative path ✓ Location of the imported specification.
as simple identifier ✓ Alias; imported names are referenced as alias.Name.
version SemVer range – Acceptable versions of the imported language (for example "^2.1.0").
integrity string – Subresource-integrity hash ("sha256-…"). Runtimes MUST verify it when present and SHOULD require it for remote URIs.
include string[] – Layers to import: any of "metamodel", "notation", "forms", "functions", "constraints", "toolbox". Default: all.
doc Doc – Why the import exists.

Imported types may be extended ("extends": "base.Element"), imported styles, shapes and markers referenced, and imported constraints are active unless the import lists include without "constraints". Import cycles are a specification error. Names are never merged implicitly: a local State and lib.State are different types.

The DISL standard library is always available under the reserved alias std without an import (Appendix B): std.roundedRect, std.arrowFilled, std.grid10, and so on. Built-in names MAY also be referenced without the std. prefix when no local name shadows them.

3.4 Functions

User-defined functions make repeated CEL logic reusable and documented.

{
  "functions": {
    "displayName": {
      "params": [{ "name": "e", "type": "Element" }],
      "returns": "string",
      "cel": "has(e.name) && e.name != '' ? e.name : e.type + ' ' + e.id.substring(0, 4)",
      "doc": "The human-readable name of an element, falling back to type and short id."
    },
    "workingDaysBetween": {
      "params": [{ "name": "a", "type": "timestamp" }, { "name": "b", "type": "timestamp" }],
      "returns": "int",
      "cel": "workingDays(a, b, 'project')"
    }
  }
}
Property Type Req. Description
params {name, type, doc}[] ✓ Positional parameters; type is a CEL type name or a metamodel type name.
returns string ✓ CEL result type.
cel string ✓ Body; may reference parameters, globally available variables of the calling context are not visible (functions are pure over their parameters, plus diagram and env when declared in uses).
uses string[] – Context variables the function may read: "diagram", "env".
recursion {maxDepth, atMaxDepth} – Lets the function call itself (below). maxDepth is an int ≥ 1; atMaxDepth is CEL source over the same parameters.
doc Doc – Documentation.

Functions are called as displayName(self). A function MAY call functions declared before it in the specification order. A function without recursion MUST NOT call itself, and indirect recursion (a function reached again through others) is a specification error in every case. This preserves CEL's termination guarantee.

Bounded recursion. A function with recursion MAY call itself. Recursion depth counts the nested calls of the same function within one top-level call, the top-level call being depth 1. A call that would be nested deeper than maxDepth MUST NOT evaluate the body; it returns the value of atMaxDepth, evaluated over that call's parameters. The run-time cost limit of 2.5 applies to the whole evaluation, including every nested call, and validators SHOULD warn when they cannot bound the estimated cost statically. A guard against cycles in the model is a parameter the function passes down (a list of visited ids), not a construct.

"expressionText": {
  "params": [ { "name": "n", "type": "string" }, { "name": "visited", "type": "list(string)" } ],
  "returns": "string", "uses": ["diagram"],
  "recursion": { "maxDepth": 2, "atMaxDepth": "'…'" },
  "cel": "n in visited ? '…' : cel.bind(ts, diagram.nodesOfType('Triple').filter(t, t.s == n), ts.exists(t, t.p == OWL_SOME) ? '∃ ' + owlShort(ts.filter(t, t.p == OWL_ON_PROPERTY)[0].o) + '.' + expressionText(ts.filter(t, t.p == OWL_SOME)[0].o, visited + [n]) : owlShort(n))"
}

This is the text of an OWL class expression on a canvas card, two levels deep; the grid's full text is a second function with a larger maxDepth.

3.5 Viewpoints

A language may offer several diagram kinds over the same model — for example a structure diagram and a timeline of the same project. Each viewpoint selects which types appear, which coordinate system and layout apply, and which toolbox groups are offered. If viewpoints is absent, a single implicit viewpoint main shows everything.

Property Type Description
label, doc, icon Display name, documentation, icon.
coordinateSystem name Coordinate system of diagrams of this kind (section 5).
include TypeRef[] Types shown. Default: all.
exclude TypeRef[] Types hidden.
members Expression → bool Whether an element whose type passes include and exclude belongs to views of this viewpoint; context element. Default true.
toolbox string[] Toolbox group ids offered. Default: all groups.
layout name or LayoutConfig Layout used (section 10).
notation object Per-viewpoint notation overrides: nodes, edges, styles maps merged over the global notation.
canvas Canvas Background, bounds and page settings (6.13).
variantOf viewpoint name This viewpoint is an alternative presentation of the named one, offered as a toggle on its views and not as a diagram kind of its own (below).
toggle {label, icon, position} The switch of a variant: label a Message (default: this viewpoint's label), icon an IconRef, position a Position outside the drawing surface.
default boolean The viewpoint used for new diagrams. Exactly one viewpoint SHOULD be default.

Membership. An element belongs to a view when its type passes include and exclude and, where the viewpoint declares members, the expression holds for it; this is step 1 of what is drawn (6.1). A relation is drawn only when both its ends are drawn. Model elements not shown in any view still exist in the model and are still validated. Membership is computed from the model and never stored. Include and exclude lists written with wildcards are matched with matchesGlob (12.4):

"functions": {
  "inView": {
    "params": [{ "name": "e", "type": "Element" }], "returns": "bool", "uses": ["diagram"],
    "cel": "diagram.includes.exists(i, i == '*' || matchesGlob(e.id, i)) && !diagram.excludes.exists(x, matchesGlob(e.id, x))"
  }
},
"viewpoints": {
  "container": { "include": ["Person", "SoftwareSystem", "Container"], "members": "inView(self)", "default": true }
}

A C4 container view draws the people, systems and containers its include and exclude statements name; relationships between members that are not drawn are lifted by a derived relation (4.11).

Variants. A viewpoint with variantOf is a variant of its base viewpoint, such as a compact presentation of the same diagram. A view whose viewpoint has variants MUST open in the base viewpoint, and the runtime MUST offer each variant as its toggle. Which of them a viewer sees is viewer state (the view-data kind viewpoint, 11.6): it MUST NOT be persisted and MUST NOT enter the undo history. A DID view MUST NOT name a variant as its viewpoint, and a new diagram MUST NOT be created in one. Edits made while a variant is shown are ordinary edits of the model. A variant MUST NOT itself have variants.

"compact": { "label": "Compact", "variantOf": "trueTime",
             "toggle": { "label": "Compact", "position": "outside-top-left" },
             "coordinateSystem": "hypeCycle" }

3.6 Layer overview (informative)

Layer overview. The eight DISL layers from top to bottom. Layer 1, metamodel, says what exists. Layer 2, coordinates, where it can be. Layer 3, notation, how it looks. Layer 4, toolbox and forms, how it is created and edited. Layer 5, constraints, what is allowed. Layer 6, behavior, what happens. Layer 7, layout, how it is arranged. Layer 8, persistence, how it is stored. Cutting across all layers are doc on everything, CEL everywhere, functions, plugins and viewpoints.
Diagram as text (Mermaid)
---
config:
  flowchart:
    wrappingWidth: 400
    rankSpacing: 12
---
flowchart TB
  accTitle: Layer overview
  accDescr: The eight DISL layers from top to bottom. Layer 1, metamodel, says what exists. Layer 2, coordinates, where it can be. Layer 3, notation, how it looks. Layer 4, toolbox and forms, how it is created and edited. Layer 5, constraints, what is allowed. Layer 6, behavior, what happens. Layer 7, layout, how it is arranged. Layer 8, persistence, how it is stored. Cutting across all layers are doc on everything, CEL everywhere, functions, plugins and viewpoints.
  L1["<b>Layer 1 · metamodel</b><br/>types · relations · attributes<br/><i>what exists</i>"]
  L2["<b>Layer 2 · coordinates</b><br/>axes · systems · placement · snap<br/><i>where it can be</i>"]
  L3["<b>Layer 3 · notation</b><br/>theme · styles · shapes · markers<br/>nodes · edges · labels · ports<br/><i>how it looks</i>"]
  L4["<b>Layer 4 · toolbox · forms</b><br/><i>how it is created/edited</i>"]
  L5["<b>Layer 5 · constraints</b><br/><i>what is allowed</i>"]
  L6["<b>Layer 6 · behavior</b><br/>hooks · operations · policies<br/><i>what happens</i>"]
  L7["<b>Layer 7 · layout</b><br/><i>how it is arranged</i>"]
  L8["<b>Layer 8 · persistence</b><br/><i>how it is stored</i>"]
  X["<b>cross-cutting</b><br/>doc on everything · CEL everywhere<br/>functions · plugins · viewpoints"]
  L1 ~~~ L2 ~~~ L3 ~~~ L4 ~~~ L5 ~~~ L6 ~~~ L7 ~~~ L8 ~~~ X
  classDef crossCutting stroke-dasharray:4 4
  class X crossCutting

Dependencies only point upward: notation refers to metamodel types, constraints to metamodel and coordinates, persistence to metamodel and coordinates — never the reverse. A headless validator therefore only needs layers 1, 2, 5 and 8.

Referenced from