커밋 d894fc0의 AUTHORING.md를 그대로 옮긴 것으로, 본문은 아직 영어입니다. 패키지는 0.1.0에서 쓸 typeshade라는 이름으로 가져옵니다.
값과 변경
Plain const — let the emit decide let / var / inline
Author every intermediate as a plain JS const. You do not wrap it in Let(...) or
Var(...):
const ab = b.sub(a)const len2 = dot(ab, ab)The emit pass decides whether each becomes an inlined expression, a shared WGSL let
(common-subexpression cache), or a var. If you later mutate a const (see .assign
below), the auto-var pass materialises it as a WGSL var automatically — no marker
needed:
const min_dist = f32(1e10) // plain const…// …later, inside a loop…min_dist.assign(min(min_dist, d)) // …auto-materialises as `var`Let(...) / Var(...) still exist for the rare case where you need to force a named
binding (a derivative like fwidth that WGSL requires in uniform control flow, or a
mutable accumulator you want to name), but the default is a plain const.
When a Let is load-bearing, not stylistic: CSE cannot hoist a subexpression
that reads a mutated var — the value differs per read site — so a shared
subexpression inside a mutation loop re-emits at every use unless you materialise it:
Loop( u32(0), (i) => i.lt(u32(72)), () => { const p = ro.add(rd.mul(t)) // t is mutated below → CSE can't cache anything reading it const d = Let(length(p).sub(1)) // materialise ONCE; without Let the SDF re-emits per read If(d.lt(0.001), () => Break()) t.assign(t.add(d)) },)Rule of thumb: inside a loop that mutates a var, Let any value derived from that var
that you read more than once.
GLSL target: a discarding struct-ctor argument auto-hoists, no marker needed.
ANGLE’s D3D11 backend miscompiles a GLSL ES 3.00 fragment shader whose struct-constructor
argument contains a call to a function that (transitively) executes discard —
COMPILE_STATUS and LINK_STATUS both report success, and the draw silently drops geometry at
the first submit. A GLSL-only legalize pass (glsl-legalize.ts) detects that shape and binds
the argument to a fresh _dhN local immediately before the constructor call, on every GLSL
emit. You keep authoring a plain const (or an inline call) as usual — there is nothing to
wrap in Var()/Let() for this; the hoist is automatic and GLSL-only, so WGSL emit is
byte-untouched.
.assign(v) — the one mutation method
JS cannot overload =, so mutation is a method on the lvalue Node (mirrors three.js TSL’s
.assign):
x.assign(value) // x = value;winding.assign(winding.add(1)) // compound = the pure op + assign; a Node has no addAssigno.pos.assign(vec4(pos, 0, 1)) // member targets work tooThere is no free assign(x, v) function in the authoring surface — .assign is a
method on the target Node. There is no compound .addAssign method on a Node either:
add is the pure expression, so x += v is x.assign(x.add(v)). (The optional Builder
handed to an fn body does carry b.assignOp(target, '+', v) / b.addAssign(target, v),
which emit a compound += statement — the same value, one statement form.)
Mutating an immutable binding is a compile error. .assign lives only on the mutable
node type (Node) returned by Var() and by every produced value (literals, ctors, arithmetic,
accessors — so the plain-const auto-var pattern works). Let(), a function param, and a module
const return the read-only supertype ReadonlyNode, which has no .assign — so someLet.assign(…)
or param.assign(…) is rejected by tsc, not just at device.createShaderModule. Read APIs
(length, dot, mix, .of, an fn return, …) accept ReadonlyNode, so an immutable binding
still flows everywhere a value is read. (This is a type-only distinction — emitted WGSL is
unchanged; it mirrors RxJS Observable vs Subject.) To mutate, declare with Var().
Method ops + contextual literal lift
Arithmetic, comparison, bitwise, swizzle, and index are methods on a Node:
| category | methods |
|---|---|
| arithmetic | .add .sub .mul .div .mod .neg |
| comparison | .lt .gt .le .ge .eq .ne |
| logical | .and .or |
| bitwise | .bitAnd .bitOr .bitXor .shl .shr |
| components | .x .y .z .w · .r .g .b .a · .rgb .xy .xyz … · .swizzle<R>('zxy') |
| index | .at(i, elemType) |
| ternary | cond.select(a, b) (WGSL select) |
The
.modMETHOD is%— trunc-mod on floats (native on WGSL; the GLSL writer spells ita - b * trunc(a / b)because GLSL ES 3.00’s%is integer-only). For float FLOOR modulo use the free functionmod(x, y): FLOOR-mod with identical semantics on both targets (WGSL spells it inline asx − y·⌊x/y⌋, GLSL as nativemod()), so negative operands wrap into[0, y)— what domain repetition and angle folds need. Named after GLSL/TSLmod— deliberately notfmod, which in C/HLSL is trunc-mod. Component-wise;ymay be a scalar broadcast over a vectorx.
A bare number literal lifts to the operand’s type from context — drop the f32() /
u32() / i32() wrapper:
x.add(1) // f32 x → `x + 1.0`flags.bitAnd(1) // u32 flags → `flags & 1u` (typed from the LHS)mode.eq(2) // u32 → `mode == 2u`vec4(pos, 0, 1) // numeric components lift to the vec's element (f32)vec2u(0, 1) // → u32 componentsThe same lift applies inside vector/struct constructors (vec2/vec3/vec4/vec2u/vec2i,
construct) and inside min/max/clamp/mix/pow/smoothstep. You only keep an explicit
f32(0.5) / u32(16) when there is no context to infer from (a standalone constant or
the type-anchor first arg of a math built-in).
Negative literals lift too — x.mul(-6), .add(-0.25), vec3(-1, 0, 1) all
emit the signed literal directly (x * -6.0) on both targets. There is no need for the
defensive .neg() / .sub() spellings some older examples used; write the sign in the
number.
radians() / degrees()
Use the WGSL built-ins for degree↔radian conversion, not a multiply by a rounded constant:
const lonRad = radians(lon) // was: lon.mul(DEG2RAD)const latDeg = degrees(latRad) // was: latRad.div(DEG2RAD)(DEG2RAD survives only as the (DEG2RAD·EARTH_R) divisor in the abs-Mercator → degree
reverse paths, where folding it out would shift precision.)