제어 흐름

커밋 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 chain
Switch(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-arm
const 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.

이 페이지 편집