Imports and Namespaces
How import assembles several files into one translation unit — recognition, specifier normalisation, source roots and root qualifiers, cycles, and line mapping.
import inlines another DreamShaderLang source file into the current translation unit. It belongs to
neither grammar: the editor source loader strips it line by line, before the declaration parser
is ever called, so the parser does not know it as a keyword.
<file>.dsm
│
├─ editor source loader strip `import` lines, inline targets depth-first,
│ enforce the .dsh / .dsf content rules
│
├─ declaration parser blocks, sections, statements -> definition tree
│
└─ material generator Graph expression grammar -> material nodes -> assetSyntax
import "<specifier>" [;] [// <comment>]
import '<specifier>' [;] [// <comment>]
<specifier> := [ <root-qualifier> ":" ] <path>
<root-qualifier> := Project | Plugin.<PluginName> | Plugins.<PluginName>The directive must be the first thing on its line — leading whitespace is allowed, anything else is
not. After the closing quote only an optional ; since 1.2.2 and an optional // comment
may follow. import itself is matched case-insensitively.
import "Shared/Common"; // -> Shared/Common.dsh in this file's own root
import '@typedreammoon/dream-noise/Library/Noise.dsh'; // -> <root>/Packages/@typedreammoon/…
import "../Functions/F_Tint.dsf" // -> relative to the importing file
import "Plugin.MoonToon:Shared/Toon.dsh"; // -> another root, see below <Since v="1.6.0" />A bare <path> resolves inside the importing file's own source root. The <root-qualifier>
prefix since 1.6.0 is the one thing that lets an import reach a different root.
Recognition
Each physical line is tested against these rules in order. A line failing any of them is not
an import and is passed to the parser unchanged.
| # | Rule |
|---|---|
| 1 | the trimmed line must not start with // |
| 2 | the trimmed line must start with import, ignoring case |
| 3 | the character after import must be whitespace, unless the line is exactly import — this is what rejects importfoo |
| 4 | the remainder, trimmed, must start with " or ' |
| 5 | the closing quote must match the opening one; a \ inside the quotes escapes the next character |
| 6 | an unterminated quote makes the line an ordinary line, not an error |
| 7 | after the closing quote, an optional ; may follow |
| 8 | after that, the rest of the line must be empty or start with // |
| 9 | the extracted specifier must be non-empty after trimming |
Rule 1 only knows about //. An import line inside a /* … */ block comment is still
honoured — the loader has no notion of block comments. Commenting a block of imports out with
/* … */ silently keeps importing them. Use // on each line instead.
An import must be alone on its line. Shader(Name="X") import "Common.dsh"; is not recognised, and
the text reaches the parser as written — where the stray import fails with
Unexpected token near index {Index}.
Specifier normalisation
| # | Step |
|---|---|
| 1 | trim leading and trailing whitespace |
| 2 | replace every \ with / |
| 3 | strip all leading ./ sequences |
| 4 | if the result has no extension at all, append .dsh |
So import "Shared/Common" and import "Shared/Common.dsh" are the same directive. Importing a
.dsf or a .dsm therefore requires the explicit extension (.dsf since 1.3.5).
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. 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
Three candidate paths are tried in order. Each is paired with a containment root; a candidate resolving outside its root is skipped rather than reported, and the first candidate that exists on disk wins.
| # | Candidate | Containment 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 |
| Directory | Default | Project setting |
|---|---|---|
| Project source | <Project>/DShader | Source Directory |
| Plugin source since 1.6.0 | <Plugin>/DShader | Scan Plugin Source Directories |
| Packages | <source directory>/Packages | derived; not separately configurable |
Source roots
since 1.6.0Candidates 2 and 3 belong to the owning root of the importing file — the source root the file sits under.
An unqualified specifier never leaves its root. 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.
Crossing a root deliberately
since 1.6.0Prefix the specifier with a root qualifier and a :.
| Specifier | Resolved against |
|---|---|
Project:Shared/Common.dsh | <Project>/DShader/Shared/Common.dsh, then <Project>/DShader/Packages/… |
Plugin.MoonToon:Shared/Toon.dsh | <MoonToon>/DShader/Shared/Toon.dsh, then <MoonToon>/DShader/Packages/… |
Plugins.MoonToon:Shared/Toon.dsh | the same — Plugin/Plugins, .// all spell the same thing |
Plugin/MoonToon:Shared/Toon.dsh | the same |
The vocabulary is the one Root= already uses, minus the
package-only spellings, and the qualifier is matched case-insensitively. Project names the project
root whatever Source Directory points at.
A qualified specifier is rooted by construction, so only two candidates are tried — the target root's source directory and its packages directory, in that order — and both are containment-checked the same way. There is no relative-to-the-importing-file candidate, and the importing file's own root is not consulted.
The : is what makes the form unambiguous. Without it, Plugin.MoonToon/Shared/Common.dsh could not
be told apart from an ordinary relative path through a folder named Plugin.MoonToon, and the
resolver would be back to guessing — the very thing the same-root rule exists to prevent. : cannot
appear in a path segment on Windows, so no real specifier collides with it.
Text before a : that does not match one of the qualifier shapes is not treated as a qualifier
at all; the whole specifier is resolved as an ordinary path. That is what keeps
import "C:/Shared/Common.dsh" failing the way it always did instead of reporting an unknown source
root. A specifier that does match the shape but names a root that is not present —
Plugin.NotInstalled:X.dsh — gets the dedicated diagnostic below.
A plugin that qualifies its way into Project: is no longer self-contained: ship it to another
project and the import dangles. The form is deliberately verbose and greppable for that reason.
The containment comparison is case-insensitive on every platform; whether a candidate is then found still follows the file system's own case behaviour.
Containment is what stops a specifier 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 too; - from a file under
DShader/Packages/@scope/pkg/,..may traverse anywhere insideDShader/Packages, because that is the containment root chosen for it; - for a source file under neither directory the containment root is its own directory, so no
..specifier can resolve 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.
Candidates 1 and 2 are still tried first, so a file of that name next to the importing file, or under
DShader itself, shadows the package copy.
See Packages for the layout this convention assumes.
Inlining, cycles and ordering
The loader walks the import graph depth-first and produces one flat text for the parser.
| Behaviour | Rule |
|---|---|
| order | an import is fully inlined before the rest of the importing file is emitted, so a dependency always precedes its dependent |
| diamonds | a file already inlined anywhere in this translation unit is skipped silently — its text appears exactly once |
| cycles | re-entering a file that is still being inlined fails with DreamShader import cycle detected at '{Path}'. |
| unreadable files | DreamShader could not read '{Path}'. |
| unresolved specifiers | DreamShader import '{Specifier}' referenced from '{Path}' could not be resolved. |
| a qualifier naming no live root since 1.6.0 | DreamShader import '{Specifier}' referenced from '{Path}' names source root '{Qualifier}', which is not a DreamShader source root. — a misspelt plugin name, a disabled plugin, or one that ships no DShader folder |
Each file's contribution is wrapped in marker comments, and every import line is replaced by an empty line so the lines below it keep their original numbers:
// Begin DreamShader source: <Project>/DShader/Shared/Common.dsh
Namespace(Name="Common")
…
// End DreamShader source: <Project>/DShader/Shared/Common.dsh
// Begin DreamShader source: <Project>/DShader/Materials/M_Water.dsm
<- blank lines where the imports were
Shader(Name="Materials/M_Water")
…
// End DreamShader source: <Project>/DShader/Materials/M_Water.dsmBecause the whole closure becomes one parse unit, the "at most one Shader block" rule is
closure-wide. Importing two files that each declare a Shader fails with
Only one top-level Shader block is currently supported., even though neither file breaks the rule
on its own.
The .dsh / .dsf content rules are applied to each file's own text, not to the assembled
closure. A .dsh may import a .dsf that declares ShaderFunction blocks, and those blocks are
compiled as part of the translation unit. See Source Files.
Namespaces across files
A Namespace is not a module and has nothing to do with import. Because the closure is one flat
text, namespaces from imported headers are visible with no further declaration — and they collide
across files exactly as they would within one.
// DShader/Lib/Common.dsh
Namespace(Name="Common")
{
Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
result = color * tint;
}
}import "Lib/Common.dsh";
Graph = {
vec3 Tinted;
Common::ApplyTint(Base, Tint, Tinted);
}| Rule | Consequence |
|---|---|
| Re-opening is allowed and unchecked | Two Namespace(Name="Common") blocks in two imported headers both prefix Common::. |
| Members are reachable only by their qualified name | There is no using, no name import, and no unqualified fallback. |
Writing import inside a Namespace body has no scoping effect | The directive is stripped line by line and its target inlined ahead of the whole file. Put imports at file scope. |
| A duplicate qualified name is caught late | Not at import time — when the generated include is written. |
The declaration rules, :: resolution and the body-normalisation trap are on
Functions.
Source-line mapping
Diagnostics are mapped back from the assembled text to the file you actually wrote.
- The mapper scans the error text for the literal
near indexand reads the integer that follows. That is the only channel by which a parse error carries a position — which is why so many messages end innear index {Index}. - It then walks the assembled text, tracking the current
// Begin DreamShader source:file and a per-file line counter that resets at each marker. Marker lines do not advance the counter. - A located message is formatted
<file>(<line>,<column>): <message>; when mapping fails the form is<file>: <message>. Grapherrors are anchored separately: the parser records where eachGraphbody starts, and a graph-relative line and column are offset onto that origin. The column offset applies only to the body's first line.
Three limits are worth knowing when a reported position looks wrong.
- Errors raised inside a section body carry an index relative to that body, but the mapper treats every index as an offset into the assembled text. Positions for in-section errors are therefore not reliable.
- An index landing exactly on a line's first character can be attributed to the previous line.
- Most statement-level messages carry no
near indexat all and are reported as<file>: <message>with no line or column.
Organising a project
| Change | What gets recompiled |
|---|---|
a .dsm changed | that material, plus every function asset the file declares |
a .dsf changed | the function assets it declares |
a .dsh changed | the .dsm / .dsf files that import it, directly or indirectly |
| a package file changed | the sources that import it |
A single stable entry header per project keeps that graph shallow:
// DShader/Shared/Common.dsh
import "Shared/Color.dsh";
import "Shared/Texture.dsh";
Namespace(Name="Project")
{
Function ApplyTint(in vec3 color, in vec3 tint, out vec3 result) {
result = color * tint;
}
}// every material imports one file
import "Shared/Common.dsh";Diagnostics
| Message | Cause | Fix |
|---|---|---|
| DreamShader import '{Specifier}' referenced from '{Path}' could not be resolved. | None of the three candidates existed, or every existing one was outside its containment root. | Check the extension: an unsuffixed specifier gets .dsh, so a .dsf or .dsm must be spelled out. |
| DreamShader import '{Specifier}' referenced from '{Path}' names source root '{Qualifier}', which is not a DreamShader source root. | A root-qualified import whose qualifier matched the grammar but named no live root — a misspelt plugin name, a disabled plugin, or one shipping no DShader folder. | Check the plugin name, and that the plugin is enabled and ships a DShader folder. |
| DreamShader import cycle detected at '{Path}'. | A file imported itself, directly or transitively. | Move the shared declarations into a third header both files import. |
| DreamShader could not read '{Path}'. | The resolved file could not be loaded. | |
| Only one top-level Shader block is currently supported. | Two Shader blocks in the closure — often one imported by accident. | Details |
| Unexpected token near index {Index}. | An import line reached the declaration parser, because it was not alone on its line. | |
| DreamShader header '{Path}' may only declare Function/Namespace/GraphFunction/VirtualFunction blocks and imports. | An imported .dsh breaks its own content rule. | Details |
| DreamShader function file '{Path}' may only declare imports, Function/Namespace/GraphFunction/VirtualFunction blocks, and ShaderFunction/ShaderLayer/ShaderLayerBlend blocks. | An imported .dsf breaks its own content rule. | Details |