Raw statements
On this page

Rendered from AUTHORING.md at commit 26de7be8. The package is imported here by its 0.2.0 name, typeshade.

Raw statements

After this page you can splice a hand-written statement into a module and know what that statement costs you on each target.

A raw statement is a string the DSL writes into a function body without reading it. Reach for one when a statement has to be hand-written, when it arrives pre-built from another generator, or when it uses a construct the IR does not model. Everything else on this page follows from one fact: to the compiler that string is opaque bytes.

A spelling per target

A payload is the { wgsl, glsl } object you hand to rawStmt, one spelling per target. rawStmt returns a statement node you can drop into a body array:

import { rawStmt, vec4fT, type FuncDecl } from 'typeshade'
const PAIRED = rawStmt({
wgsl: 'return vec4<f32>(1.0, 0.0, 0.0, 1.0);',
glsl: 'return vec4(1.0, 0.0, 0.0, 1.0);',
})
const fs: FuncDecl = {
name: 'fs_main',
attrs: ['@fragment'],
stage: 'fragment',
params: [],
ret: vec4fT,
retAttr: '@location(0)',
body: [PAIRED],
}

The meaning of a raw is fixed, which is “put these bytes here”. Only the spelling is per target. emitModule writes the wgsl string and emitGlslModule writes the glsl string, each one verbatim, and neither writer ever emits the other’s side.

Inside an fn body

A fn body is assembled by a builder, so splice a raw there with b.raw(payload). The builder is the second argument the body receives:

import { fn, voidT } from 'typeshade'
const seed_lane = fn('seed_lane', {}, voidT, (_p, b) => {
b.raw({ wgsl: 'let _k = 1.0;', glsl: 'float _k = 1.0;' })
})

A bare rawStmt(...) call inside a fn body is a discarded expression: it builds a node, nobody pushes it, and nothing is emitted. Use the free factory when you assemble a Stmt[] array by hand, as the first sample does, and b.raw everywhere else. b.raw goes through the same push path as every other statement, so it records a source location like the rest of the body.

A missing side fails closed

At least one side is required at the type level, so rawStmt({}) is a compile error. Supplying only one side is allowed, and it is a decision: this module does not build for the other target. A backend handed a raw with no spelling for its own target throws UnsupportedFeatureError, coded SD0030. The message names the spelling that is missing and quotes the side you did give, so the error points at the statement to port.

import { emitGlslModule, emitModule, fn, module, vec4, vec4fT } from 'typeshade'
const fs = fn(
'fs_main',
{},
vec4fT,
(_p, b) => {
b.raw({ glsl: 'gl_FragDepth = 0.5;' })
return vec4(1, 0, 0, 1)
},
{ stage: 'fragment' },
)
const m = module({ funcs: [fs] })
emitGlslModule(m, 'fragment') // splices the glsl spelling verbatim
emitModule(m) // throws UnsupportedFeatureError (SD0030)

This is the shape to use when you ship WebGL2 today and may add WebGPU later. The GLSL build keeps working, and the day someone runs the WGSL writer it stops at the first raw that has no wgsl spelling, naming the statement to port. Supply every spelling the module must build for, and the question never comes up.

Indentation and identifiers

Two things about the text itself are yours to get right.

The writer prepends the enclosing body indent to the spelling as a whole, so only the first line of a multi-line spelling is indented and every later line lands at column 0. Indent the continuation lines yourself when the shape of the emitted source matters:

b.raw({
wgsl: 'if (t > 1.0) {\n t = 1.0;\n }',
glsl: 'if (t > 1.0) {\n t = 1.0;\n }',
})

Identifiers inside a spelling are also yours to keep valid, because nothing reads into the string and so nothing rewrites it. The GLSL backend renames params and locals whose names collide with a GLSL reserved word, in, sample, filter and texture among them, so a glsl spelling that names one of those refers to a variable the emitted stage no longer has. The WGSL side has no such renamer, so only the GLSL spelling is exposed to it, even though the contract is the same in both directions. Read the emitted source once after you add a raw that mentions a name from the surrounding body.

What a raw statement turns off

Three things a module gives up, module wide, from a single raw anywhere in it.

The identifier mangler declines to run. mangle(), from Production emit, shortens helper, struct and const names and hands back a map from authored names to emitted ones. Given a module that holds a raw, it returns the module unchanged and an empty map, because renaming around text it cannot read would desync the splice:

import { emitModule, f32T, fn, module, vec4, vec4fT } from 'typeshade'
import { mangle } from 'typeshade/emit-prod'
const shade = fn('shade_pixel', { x: f32T }, vec4fT, (p) => vec4(p.x, 0, 0, 1))
const fs = fn(
'fs_main',
{},
vec4fT,
(_p, b) => {
b.raw({ wgsl: 'let _k = 1.0;', glsl: 'float _k = 1.0;' })
return shade(0.5)
},
{ stage: 'fragment' },
)
const renames = new Map<string, string>()
const wgsl = emitModule(module({ funcs: [shade, fs] }), { plugins: [mangle({ renames })] })
// The module holds a raw, so `shade_pixel` keeps its authored name and renames is empty.

GLSL stage scoping switches off. The GLSL backend normally emits into a stage only the functions that stage can reach. Raw text is opaque to that reference walk, so the filter bails for the whole module and every helper is emitted into every stage. A helper the entry never calls therefore still reaches the writer, and a raw in it with no glsl spelling still fails the whole GLSL emit. The sharper consequence is that fragment-only machinery in any helper, dpdx, dpdy, fwidth and discard, is emitted into the vertex stage as well, where it does not compile. Keep a raw-carrying module clear of such helpers, or move the raw into a module of its own. Entry points of the other stage are still dropped before the body walk, so enforcement stays per stage: a raw with no glsl spelling fails each stage whose emitted function set contains it.

The CPU oracle stops at the raw. compileModule accepts the module and hands back a function per fn, and the oracle has no evaluation for raw text, so calling a function whose body holds one throws. Other functions in the same module still run. If one function needs both a raw and an oracle check, split it: keep the arithmetic in a function the oracle can run and put the raw in a caller.

Edit this page Report a problem