값과 변경

커밋 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 addAssign
o.pos.assign(vec4(pos, 0, 1)) // member targets work too

There 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:

categorymethods
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)
ternarycond.select(a, b) (WGSL select)

The .mod METHOD is % — trunc-mod on floats (native on WGSL; the GLSL writer spells it a - b * trunc(a / b) because GLSL ES 3.00’s % is integer-only). For float FLOOR modulo use the free function mod(x, y): FLOOR-mod with identical semantics on both targets (WGSL spells it inline as x − y·⌊x/y⌋, GLSL as native mod()), so negative operands wrap into [0, y) — what domain repetition and angle folds need. Named after GLSL/TSL mod — deliberately not fmod, which in C/HLSL is trunc-mod. Component-wise; y may be a scalar broadcast over a vector x.

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 components

The 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 toox.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.)

이 페이지 편집