DreamShaderLang
Language

Imports and Namespaces

How import makes a .dsh header's declarations visible in the current file — recognition, specifier normalisation, source roots, how a header is read, and namespaces across files.

import

DreamShader » DreamShaderLang » import

A top-level line that makes the declarations of a .dsh header visible in the current file.

Declared in.dsm, .dsf, .dsh; a .dss spells it #include "…" and takes import "…"; as well
Kindtop-level declaration
Namesa .dsh header (since 2.0.0; through 1.9.x also a .dsf)
Processed bythe legacy front end, which reads the line; the binder, which resolves it through the include resolver and declares the header's names into this file (since 2.0.0; through 1.9.x the editor's source loader pasted the header's text in before parsing)
Case ruleimport is lower case; another casing is read the same, with the warning DSH2253

Synopsis

import "<path>" [;]
import '<path>' [;]          // in a .dsm, .dsf or .dsh

<path> := a path to a .dsh, relative to this file or to its source root,
          or "/"-anchored; the ".dsh" may be left off

A path resolves inside the importing file's own source root — see Roots. The root-qualified form of 1.6.0 (Project:…, Plugin.<Name>:…) is refused (since 2.0.0).

Recognition

(since 2.0.0) import is a token of the language, read by the parser like any other top-level declaration. The 1.x rule that recognized it line by line, in the raw text, is gone.

Rule
whereat the top level of the file, between blocks
commentsordinary comments: an import inside /* … */ or after // is commented out (the 1.x loader honoured one inside /* … */)
the patha string: "…", with the usual escapes; in a 1.x file or a .dsh also '…' on one line
;optional
#ifresolved first, by the preprocessor: an import in a branch that is not taken is not read
RefusedCode
import not followed by a stringDSH2252
a path that names a .dsf or a .dsm — a material or function file is compiled on its own, never includedDSH2252
a root-qualified path (Project:Shared/Common.dsh, Plugin.X:…)DSH2252
a path that resolves to a file that is no .dsh (or .dss)DSH8295

[!NOTE] The editor's dependency scanner still reads import lines as text, to know which sources to recompile when a header is saved: a trimmed line that starts with import, ignoring case, then a "…" or '…' path, an optional ; and nothing but a // comment. It reads the raw file, #if branches included, so a header imported only from a branch that is not taken still triggers a rebuild of its importers. That is deliberate: a needless rebuild costs seconds, a missed one leaves a stale asset.

Specifier normalization

The path is normalized before resolution:

#Step
1trim leading and trailing whitespace
2replace every \ with /
3strip all leading ./ sequences
4if the result has no extension at all, append .dsh

So import "Shared/Common" and import "Shared/Common.dsh" are the same directive.

[!NOTE] Step 4 asks whether the path has an extension, not whether it has a known one, and it looks only at the last path segment. A file name containing a . — Shared/Common.v2 — counts as "already has an extension", so no .dsh is appended and the specifier resolves only if a file with exactly that name exists. A . in a directory component — @scope/pkg.v2/Lib — does not count, and .dsh is still appended.

Resolution

PathResolved against
an absolute file path that existsthat file
/-anchored (since 2.0.0): /Shared/Common.dsh, or /Game/Shared/Common.dshthe anchor (/ or /Game/) dropped, then the importing file's own root — its source directory, then its packages directory — then every other root's two directories, in root order; the first file that exists wins. Never relative to the importing file
anything elsethe three candidates below

The three candidates are tried in order. Each is paired with a containment root; a candidate that resolves outside its root is skipped rather than reported, and the first candidate that exists on disk wins.

#CandidateContainment root
1<directory of the importing file>/<specifier>the longest source or packages directory, across every root, that contains the importing file; the importing file's own directory when it is under none
2<owning root's source directory>/<specifier>that source directory
3<owning root's packages directory>/<specifier>that packages directory
DirectoryDefaultProject setting
Source<Project>/DShaderSource Directory
Packages<Source>/Packagesderived; not separately configurable

The containment comparison is case-insensitive on every platform. Whether a candidate is then found still goes through an ordinary file-existence check, which follows the file system's own case behaviour.

A path that resolves nowhere is DSH8292, naming the reason.

Roots

Candidates 2 and 3 belong to the owning root of the importing file — the source root the file sits under. A relative specifier never leaves it: a .dsm under Plugins/MoonToon/DShader cannot reach <Project>/DShader by writing Shared/Common.dsh, and vice versa.

The rule exists so that adding a plugin cannot change what an existing import means. Were the project root a global fallback, two plugins shipping Shared/Common.dsh would resolve by scan order, and disabling a plugin would silently redirect another root's imports.

A file under no root — a test fixture, a commandlet -Source pointing outside the tree — takes the project's source and packages directories for candidates 2 and 3, which is what every file did before roots existed.

See Source files for the root list and how plugins contribute to it.

Crossing a root deliberately

Through 1.9.x a root qualifier and a : — Project:Shared/Common.dsh, Plugin.MoonToon:Shared/Toon.dsh (since 1.6.0) — pointed an import at another root's source and packages directories. An import refuses that form (since 2.0.0) with DSH2252. A header another root ships is reached by a /-anchored path, which looks in the importing file's own root first and then in every other root — so a file of the same name in the own root wins — or by copying it into this root, or through a package.

[!WARNING] A plugin that reaches into the project root is no longer self-contained: ship it to another project and the import dangles.

The containment check is what stops a relative specifier from climbing out of the tree. .. segments are resolved before the check, so:

  • from a file directly under DShader, import "../Secret.dsh" resolves above the source directory and candidate 1 is skipped; candidates 2 and 3 collapse the same .. and land outside their own roots, so they are skipped by the same check;
  • from a file under DShader/Packages/@scope/pkg/, .. may traverse anywhere inside DShader/Packages, because that is the containment root chosen for it;
  • for a source file under no root's source or packages directory, the containment root is its own directory, so no .. specifier can resolve through candidate 1 at all.

Package-style paths

@scope/name/… is not a distinct path syntax. @ is an ordinary directory-name character, and a specifier such as "@typedreammoon/dream-noise/Library/Noise.dsh" resolves through candidate 3 simply because DShader/Packages/@typedreammoon/dream-noise/Library/Noise.dsh exists on disk. There is no scope registry, no version resolution and no special-cased root.

[!NOTE] Candidates 1 and 2 are still tried first. A file named @typedreammoon/dream-noise/Library/Noise.dsh next to the importing file, or under DShader itself, shadows the package copy.

See Packages for the directory layout this convention assumes.

How a header is read

(since 2.0.0) Nothing is inlined. Each header is read on its own and its declarations are added to the importing file's.

BehaviourRule
preprocessingeach header is preprocessed on its own, against the compile's define table: a #define in a header does not reach the file that imports it. A header whose #if fails is DSH8291
parsingeach header is parsed on its own, in its own dialect: a .dsh may hold 1.x blocks and 2.0 declarations side by side, whichever kind of file imports it. A header with a syntax error reports that error at its own line, and the import is then DSH8294; one that cannot be read is DSH8293
namesevery declaration of the header — and of the headers it imports — is visible in the importing file. A name declared twice, in the header and the importing file or in two headers, is DSH4210
diamondsa header reached twice is declared once
cyclesa header that imports itself, directly or through another, is DSH4211
assetsnone: a header holds no asset block (DSH2249), so an import adds names, never an asset, and a file still holds at most one Shader (DSH2250)
rebuildsthe preprocessed text of every header a compile read is part of that source's build key, and saving a header queues every source that imports it

[!NOTE] Through 1.9.x the loader pasted the whole import closure into one text before parsing, so the "one Shader" rule spanned the closure, a .dsh could import a .dsf and compile its ShaderFunction blocks as part of the importing file, and every position had to be mapped back from the assembled text.

Source-line mapping

There is nothing to map (since 2.0.0). A diagnostic carries the file it was raised in — the header, for an error inside one — and the line and column of the construct, and is printed <file>(<line>,<column>): DSHnnnn: <message>. Through 1.9.x positions came from a near index offset into the assembled text and were unreliable inside sections; that mechanism is gone. See Diagnostics.

Diagnostics

CodeRaised when
DSH2252the line is not an import of a header: no string after import, a .dsf / .dsm path, or a root-qualified path
DSH2253(warning) import written in another case
DSH8292the path resolves to no file
DSH8295it resolves to a file that is not a header
DSH8293the header cannot be read
DSH8291the header fails conditional compilation
DSH8294the header cannot be parsed; its own errors are reported first
DSH4211the import closes a cycle
DSH4210a name the header declares is declared again
DSH2249the header holds an asset block

Example

<Project>/DShader/
├── Materials/
│   └── M_Water.dsm
├── Shared/
│   └── Common.dsh
└── Packages/
    └── @typedreammoon/
        └── dream-noise/
            └── Library/
                └── Noise.dsh
// DShader/Shared/Common.dsh
Namespace(Name="Common")
{
    Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
        result = color * tint;
    }
}
// DShader/Materials/M_Water.dsm
import "Shared/Common";                                   // -> DShader/Shared/Common.dsh
import '@typedreammoon/dream-noise/Library/Noise.dsh';    // -> DShader/Packages/@typedreammoon/...

Shader(Name="Materials/M_Water")
{
    Outputs = {
        vec3 Color;
        Base.EmissiveColor = Color;
    }

    Graph = {
        vec3 Water = vec3(0.1, 0.3, 0.6);
        Color = Common::ApplyTint(Water, vec3(1.0, 0.9, 0.8));
    }
}

The first path has no extension, so .dsh is appended; candidate 1 does not exist beside M_Water.dsm, so candidate 2 finds it under DShader. The second is found only by candidate 3. Both directives omit or include the ; freely.

Each header is preprocessed and parsed on its own; Common::ApplyTint and whatever Noise.dsh declares are then names of M_Water.dsm, and a diagnostic inside Common.dsh is reported at DShader/Shared/Common.dsh(<line>,<column>).

The .dss spelling of the same two lines:

#include "Shared/Common.dsh"
#include "@typedreammoon/dream-noise/Library/Noise.dsh"

See also

On this page