커밋 d894fc0의 AUTHORING.md를 그대로 옮긴 것으로, 본문은 아직 영어입니다. 패키지는 0.1.0에서 쓸 typeshade라는 이름으로 가져옵니다.
제어 흐름
If / elif / else — statements
If(pin.vis.lt(0), () => { Discard()})
If(p.idx.eq(1), () => { pos.assign(vec2(3, -1))}) .elif(p.idx.eq(2), () => { pos.assign(vec2(-1, 3)) }) .else(() => { /* … */ })If / elif / else bodies are zero-arg closures () => … that author into the
innermost active scope (no Builder is threaded). They are statements — a body
should not “return” a value as a fall-through; for early exits use Return() / ReturnIf().
Loop — the C-style for
Loop( u32(0), (i) => i.lt(u32(64)), // cond receives the counter… (i) => { // …and so does the BODY — declare `(i)` here too acc.assign(acc.add(toF32(i))) },)Loop (optional leading name
string names the WGSL counter; step defaults to +1). Both callbacks receive the
counter — a body written () => {}
that references i compiles as JS closure syntax but i is not in scope: tsc flags it
(Cannot find name 'i'), and a transpile-only runner (vitest) surfaces it at build time as
while building fn '…': in Loop body: i is not defined. Continue() / Break()
are the loop terminators; the counter is a mutable Node (loop-var reassignment is legal
WGSL).
Switch — statement dispatch
A chainable builder mirroring the If chain. For value dispatch, forward-declare a Var
and assign it in the case arms (the familiar imperative form):
const radiusPx = Var(rawRadius)Switch(sizeMode) .case(1, () => radiusPx.assign(rawRadius.div(viewport.z))) .case(2, () => radiusPx.assign(…)) .default(() => {}) // default is optional but terminates the chainSwitch(seg.kind) .case(0, () => { min_dist.assign(min(min_dist, dist_to_segment(uv, seg.p0, seg.p1))) winding.assign(winding.add(winding_line(uv, seg.p0, seg.p1))) }) .case(1, () => { … }) .default(() => {})Value combinators — when / reduce
When you want a branch-initialised value instead of a mutation, use the value
combinators. They take only values — no var name, no type token (the type is inferred
from the arms). when is the one condition-dispatch combinator (2-arm and N-arm), the
condition-side sibling of Switch/matchExpr (scrutinee) and select (eager 2-way):
// 2-armconst dir = when( segLen.lt(1e-6), () => vec2(1, 0), () => segVec.div(segLen),)
// N-arm: array of [condition, () => value] arms, then the else value (first true wins)const clip = when( [ [projParams.x.lt(0.5), () => transformMat4(mvp, vec4(rel2d, 0, 1))], [projParams.x.lt(6.5), () => transformMat4(mvp, vec4(relG, 0, 1))], ], () => transformMat4(mvp, vec4(ecefRtc, 1)),)
// loop fold — body RETURNS the next accumulator (no Var + assign at the call site)const best = reduce( f32(1e10), u32(0), (i) => i.le(STEPS), (acc, i) => { const q = bezierPoint(i) return min(acc, length(p.sub(q))) }, u32(1),)when/reduce materialise the var + control flow internally and return the result Node, so
the emit is identical to the hand-written var v; if (…) v = … form. Use when for genuine
condition/range dispatch (no single scrutinee); use Switch/matchExpr for integer
scrutinee dispatch. (ifExpr/condExpr are deprecated aliases of when.)
enumU32 / matchEnum — EXHAUSTIVE integer dispatch
For dispatch over a fixed set of integer cases, declare an enumU32 and use matchEnum. The
arms object must cover every member — omit one (or add an unknown key) and it is a tsc
compile error, so adding a member surfaces every un-handled site. It lowers to the same
matchExpr (switch) the hand-written form emits (byte-identical):
const Kind = enumU32({ Line: 0, Fill: 1, Stroke: 2 })
const color = matchEnum(seg.kind, Kind, { Line: () => lineColor, Fill: () => fillColor, Stroke: () => strokeColor, // drop an arm → compile error})// Kind.members.Fill is a Node<'u32'> literal; Kind.struct/values feed the case labels.Use matchEnum over a bare Switch/matchExpr whenever the case set is closed — it turns a
“forgot a case” runtime/visual bug into a compile error (the dispatch analogue of the
.assign-on-Let footgun being a type error).
Early returns — Return / ReturnIf
A control-flow body never captures a native return value as an early exit (that would
read as a silent fall-through). Make early returns explicit:
Return(value) // return value;ReturnIf(winding.ne(0), f32(1).sub(min_dist)) // if (winding != 0) { return …; }A fn body’s final return value is native TS (the body’s terminal return) — that
one is fine and is type-checked. Return() / ReturnIf() are for early exits inside
If / Loop / Switch. (fn with an early Return needs opts.allowEarlyReturn.)
Loop is the C-style for loop;
Continue() / Break() / Discard() are the loop/fragment terminators.