if, for and while read as in C, but braces are required and conditions must be bool (if (n) with an i32 gave "if statement condition must be bool, got i32"). WGSL adds loop, whose optional continuing block runs after every iteration and may end with break if (a do-while is loop { ...; continuing { break if done; } }):
// out: 3 x u32
@group(0) @binding(0) var<storage, read_write> out: array<u32>;
const pages = array(312u, 428u, 256u, 198u, 344u, 176u);
@compute @workgroup_size(1) fn main() {
var total = 0u;
for (var i = 0u; i < 6; i++) { total += pages[i]; } // for
var n = 0u;
while (n < 6 && pages[n] != 198) { n++; } // while: index of the 198-page book
var halvings = 0u;
var p = 428u;
loop { // loop with continuing
if (p < 10) { break; }
continuing { p /= 2; halvings++; }
}
out[0] = total; out[1] = n; out[2] = halvings;
}Output
out: 1714, 3, 6
&& and || short-circuit, so pages[n] is never read with n == 6. When invocations that run together take different branches, the GPU runs both sides with some lanes masked off, so prefer select() for short branches in hot loops.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="340"></canvas>
<canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
function label(text, x, y, size = 12, color = '#2b2b2b', align = 'left', weight = '') {
ink.font = `${weight} ${size}px system-ui, sans-serif`; ink.fillStyle = color; ink.textAlign = align; ink.fillText(text, x, y);
}
// The book's three loops, plus the halving sequence recorded in out[3..] for the picture.
const shared = /* wgsl */ `
const pages = array(312u, 428u, 256u, 198u, 344u, 176u);`;
const lab = shared + /* wgsl */ `
@group(0) @binding(0) var<storage, read_write> out: array<u32>;
@compute @workgroup_size(1) fn main() {
var total = 0u;
for (var i = 0u; i < 6; i++) { total += pages[i]; } // for
var n = 0u;
while (n < 6 && pages[n] != 198) { n++; } // while: index of the 198-page book
var halvings = 0u;
var p = 428u;
loop { // loop with continuing
out[3 + halvings] = p;
if (p < 10) { break; }
continuing { p /= 2; halvings++; }
}
out[0] = total; out[1] = n; out[2] = halvings;
}`;
const view = shared + /* wgsl */ `
@group(0) @binding(0) var<storage> out: array<u32>;
struct Out { @builtin(position) pos: vec4f, @location(0) color: vec3f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
var px: vec2f;
var color: vec3f;
if (i < 6) { // six spines, heights = page counts
px = vec2f(30 + f32(i) * 44 + q.x * 34, 250 - q.y * f32(pages[i]) * 0.45);
color = select(vec3f(0.08, 0.40, 0.75), vec3f(0.88, 0.55, 0.15), i == out[1]);
} else { // the halving sequence as bars
let k = i - 6;
let shown = f32(k <= out[2]);
px = vec2f(330 + f32(k) * 34 + q.x * 26 * shown, 250 - q.y * f32(out[3 + k]) * 0.45 * shown);
color = vec3f(0.55, 0.30, 0.65);
}
return Out(vec4f(px.x / 300 - 1, 1 - px.y / 170, 0, 1), color);
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }`;
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const B = GPUBufferUsage, SIZE = 16 * 4;
const out = device.createBuffer({ size: SIZE, usage: B.STORAGE | B.COPY_SRC });
const read = device.createBuffer({ size: SIZE, usage: B.COPY_DST | B.MAP_READ });
const compute = device.createComputePipeline({ layout: 'auto', compute: { module: device.createShaderModule({ code: lab }) } });
const module = device.createShaderModule({ code: view });
const render = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module }, fragment: { module, targets: [{ format }] } });
const encoder = device.createCommandEncoder();
const cp = encoder.beginComputePass();
cp.setPipeline(compute);
cp.setBindGroup(0, device.createBindGroup({ layout: compute.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: out } }] }));
cp.dispatchWorkgroups(1);
cp.end();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(render);
pass.setBindGroup(0, device.createBindGroup({ layout: render.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: out } }] }));
pass.draw(4, 6 + 8);
pass.end();
encoder.copyBufferToBuffer(out, 0, read, 0, SIZE);
device.queue.submit([encoder.finish()]);
await read.mapAsync(GPUMapMode.READ);
const o = new Uint32Array(read.getMappedRange().slice(0));
read.unmap();
label('Page counts', 30, 30, 13, '#222', 'left', 'bold');
[312, 428, 256, 198, 344, 176].forEach((p, i) => label(String(p), 47 + i * 44, 268, 11, '#444', 'center'));
label(`for: total = ${o[0]} pages`, 30, 296, 12.5, '#1f4f8a');
label(`while: the 198-page book is index ${o[1]} (orange)`, 30, 316, 12.5, '#8a5a00');
label('loop { ... continuing { p /= 2 } }', 330, 30, 13, '#222', 'left', 'bold');
for (let k = 0; k <= o[2]; k++) label(String(o[3 + k]), 343 + k * 34, 268, 11, '#444', 'center');
label(`break when p < 10: halvings = ${o[2]}`, 330, 296, 12.5, '#5b2b6e');
}
main();
</script>