Rendered from AUTHORING.md at commit 26de7be8. The package is imported here by its 0.2.0 name, typeshade.
Diagnostics
After this page you can read a coded error, branch your own code on the code it carries, get every failure in a module in one report, and print the TypeScript line an error came from.
A diagnostic is one problem found in one module: the id of the rule that found it, a
severity of error or warning, a message, and when they are available, a stable code, the
function it sits in, a one-line hint and a source location. A thrown error carries the
same message, code, hint and loc. A report entry adds the rule id, the severity and
the function it sits in.
Coded errors
Every coded failure this package raises is a TypeShadeError. It carries a code from a
frozen catalogue, a composed message, and a hint where the catalogue has a one-line
remedy for that code. ValidationError is a subclass, so one instanceof TypeShadeError
handler catches every coded failure.
import { TypeShadeError, emitModule } from 'typeshade'
try { emitModule(buildModule())} catch (e) { if (e instanceof TypeShadeError) console.error(e.code, e.message, e.hint) throw e}Type mismatches surface while you build the module, before any emit call, because the
builders check their operands as they run. Adding a vec2f to a vec3f throws SD0002.
The message opens with the code and the catalogue summary,
typeshade [SD0002]: binary op on mismatched vectors, then carries the operator and the
two types it was given, +: vec2<f32> vs vec3<f32>. Under it comes the hint line,
both operands must be the same vector type, or one must be a scalar.
Branching on a code
The code is the half of an error to write your own code against. Codes are SD####
strings from an append-only catalogue and are never renumbered, so a branch on one keeps
working. Messages compose the catalogue summary with the detail of the particular failure
and are free to be reworded, so a branch on message text does not.
try { buildModule()} catch (e) { if (e instanceof TypeShadeError && e.code === 'SD0002') { // a binary op on mismatched vectors: report it against the author's own source console.error(e.message, e.loc) } else throw e}Every failure at once
validate(m) runs the emit-time rules over an authored module. They include the structural
invariants that hold for every module: no duplicate function or struct name, no binding
collision, every path of a value-returning function returns, no mixed scalar arithmetic,
call sites that match the declarations they call, no two locals in one function sharing a
name. emitModule and the GLSL emitters run it first, so a module that breaks one of them
fails at the emit call with a coded error instead of at createShaderModule with a driver
message.
validate collects every error before it throws. The ValidationError it throws renders
them all in its message and carries the array on .diagnostics, which is the one to
present in a UI.
import { emitModule, ValidationError } from 'typeshade'
try { emitModule(m)} catch (e) { if (e instanceof ValidationError) { for (const d of e.diagnostics) console.error(d.ruleId, d.fn, d.message) } else throw e}A module that declares ramp twice and has a band function falling out of an If
without returning reports both problems in one throw:
typeshade [SD0020]: module validation failed (2 errors): - dup-func: duplicate function 'ramp' - all-paths-return (fn band): fn 'band' returns non-void but a code path falls through without returnThe diagnose report
diagnose(m) is the “what is wrong with this module?” entry. It runs the full lint
ruleset, adds a capability check when you pass a backend, and returns a report of every
diagnostic plus a summary counting them. It never throws and it never changes the module,
so it is safe to call on a module you are about to emit. formatReport renders the report
as text. lintModule(m) runs the same full ruleset and hands back the diagnostics as a
plain array, with no summary and no capability check.
The full ruleset is wider than the emit-time set: naming, nesting depth, dead bindings,
float equality, single exit, assignment to an immutable binding. Those reach you here and
nowhere else. The last one is the Let then .assign() mistake, which emits WGSL a driver
rejects, so it is worth a diagnose run before you ship a shader.
import { wgslBackend } from 'typeshade'import { diagnose, formatReport } from 'typeshade/dev'
const report = diagnose(m, { rules: 'all', backend: wgslBackend })if (report.summary.errors > 0) console.error(formatReport(report))For a rim.ts that binds edge with Let and assigns to it on line 8, the report opens
with the severity, the code, the rule and the function, then the location:
error[SD0107] no-assign-to-let (fn rim_alpha) --> rim.ts:8:14Under those two lines comes the rule’s own message, which names the binding and the function
it sits in and says that assigning to a let is invalid WGSL, then the hint,
declare the binding with Var() instead of Let() to mutate it. The run ends with its
counts, 1 error, 0 warnings. The path is the one the stack reports for the authoring file,
shortened here.
rules: 'core' narrows the run to the same set validate uses, and the backend option
adds one SD0030 diagnostic naming every capability the backend cannot cover, which is the
non-throwing twin of the gate emitModule runs.
Source locations
The --> line above is the TypeScript that built the offending statement. Capturing it is
off by default, because it costs a stack walk per authored node. With it off no stack is
walked at all, so leaving the switch in place costs nothing. Turn it on for a development
or test run:
import { setSourceTracing } from 'typeshade/dev'
setSourceTracing(true)Setting TYPESHADE_TRACE=1 in the environment turns it on for the whole process,
which is the way to get locations out of a test run without editing the test. Locations
never reach the emitted shader: WGSL and GLSL come out byte-identical with tracing on and
with it off. Because capture is optional, loc on an error and on a diagnostic is optional
too, so read it as a field that may be absent.