Diagnostics
On this page

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 return

The 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:14

Under 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.

Edit this page Report a problem