2 * Statement list -> a replayable sequence of GPU operations.
3 *
4 * Everything expensive happens once, here: pipeline compilation, buffer
5 * allocation, bind-group construction. Because numbl fixes every type and
6 * shape at lowering time, the resulting op sequence is fully static — so
7 * `encodeStep` is pure synchronous command recording, with no allocation, no
8 * pipeline lookup and no readback. That is what lets the whole timestep be
9 * encoded into one submit and keeps the CPU out of the loop.
10 */
11import { isMultiElement, scalarDouble } from 'numbl-src/numbl-core/jit/lowering/types.ts';
12import type { Assign, For, IRExpr, IRStmt } from 'numbl-src/numbl-core/jit/lowering/ir.ts';
13import type { NumericType, Type } from 'numbl-src/numbl-core/jit/lowering/types.ts';
14import { ShtPlan, type ShtBinding } from '../sht/sht.ts';
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 15import { DerivPlan, type DerivBinding } from '../sht/deriv.ts';
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 16import type { CompiledFunction } from './compile.ts';
17import { EXTERNAL_OPS } from './externals.ts';
18import {
19 buildKernel,
20 UnsupportedOnGpu,
21 WORKGROUP_SIZE,
22 type KernelInputs,
23} from './wgsl.ts';
25const isNumeric = (t: Type): t is NumericType => t.kind === 'Numeric';
26const isTensor = (t: Type): boolean => isNumeric(t) && isMultiElement(t);
27const numel = (t: NumericType): number => (t.shape ?? []).reduce((a, b) => a * b, 1);
29/**
30 * The compile-time value of a scalar expression, if it has one. A literal
31 * carries its own; a variable carries one when it was bound to a `const` (the
32 * host's fixed scalars) or computed from constants, because numbl propagates
33 * `exact` through the type lattice.
34 */
35const exactValue = (e: IRExpr): number | undefined => {
36 if (isNumeric(e.ty) && typeof e.ty.exact === 'number') return e.ty.exact;
37 return e.kind === 'NumLit' ? e.value : undefined;
38};
40/** Cap on the iterations a `for` may unroll to. Each one is real GPU work —
41 * its own pipelines at compile time and its own dispatches per step — so a
42 * runaway bound should be a clear error rather than a hang. */
43const MAX_UNROLL = 64;
45interface Slot {
46 buffer: GPUBuffer;
47 count: number;
48}
50const makeBuffer = (device: GPUDevice, label: string, count: number): GPUBuffer =>
51 device.createBuffer({
52 label,
53 size: 4 * count,
54 usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
55 });
57/**
58 * Buffers for host-bound variables, shared across plans.
59 *
60 * A model is two programs — `init` and `step` — compiled separately but
61 * operating on the same state. `U` in the step must be the very buffer `init`
62 * wrote, so the buffers for host bindings live here rather than inside either
63 * plan.
64 */
65export class HostBuffers {
66 #device: GPUDevice;
67 #slots = new Map<string, Slot>();
69 constructor(device: GPUDevice) {
70 this.#device = device;
71 }
73 ensure(name: string, count: number): Slot {
74 const existing = this.#slots.get(name);
75 if (existing) {
76 if (existing.count !== count) {
77 throw new UnsupportedOnGpu(
78 `'${name}' is ${existing.count} elements in one program and ` +
79 `${count} in another`,
80 );
81 }
82 return existing;
83 }
84 const slot = { buffer: makeBuffer(this.#device, `mgpu-${name}`, count), count };
85 this.#slots.set(name, slot);
86 return slot;
87 }
89 get(name: string): Slot | undefined {
90 return this.#slots.get(name);
91 }
93 /** Upload initial data for a host binding. */
94 upload(name: string, data: Float32Array): void {
95 const slot = this.#slots.get(name);
96 if (!slot) throw new Error(`upload: no buffer named '${name}'`);
97 if (data.length !== slot.count) {
98 throw new Error(
99 `upload '${name}': expected ${slot.count} elements, got ${data.length}`,
100 );
101 }
102 this.#device.queue.writeBuffer(slot.buffer, 0, data as Float32Array<ArrayBuffer>);
103 }
105 destroy(): void {
106 for (const s of this.#slots.values()) s.buffer.destroy();
107 this.#slots.clear();
108 }
109}
111type Op =
112 | {
113 kind: 'kernel';
114 pipeline: GPUComputePipeline;
115 bindGroup: GPUBindGroup;
116 count: number;
117 label: string;
118 /** Set when the kernel had to write to scratch because its output
119 * aliases one of its inputs; copied back after the dispatch. */
120 copyBack?: { from: GPUBuffer; to: GPUBuffer; bytes: number };
121 }
122 | { kind: 'synth' | 'analys'; binding: ShtBinding; label: string }
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 123 | { kind: 'dtheta' | 'dphi'; binding: DerivBinding; label: string }
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 124 | { kind: 'copy'; from: GPUBuffer; to: GPUBuffer; bytes: number; label: string };
126export interface PlanSpec {
127 /** The specialized function this plan executes. */
128 fn: CompiledFunction;
129 /** Output index -> host binding name to copy the result into after the run,
130 * so the next call reads it (the new spectral state feeds the old). */
131 feedback: (string | null)[];
132}
134/**
135 * Bind group layout for a kernel: the output at 0, `inputs` read-only storage
136 * buffers after it, then the params buffer.
137 *
138 * Declared explicitly rather than with `layout: 'auto'`, because an auto layout
139 * only contains the bindings the shader actually references — so a kernel that
140 * happens to use no parameters (`uuv = u .* u .* v`) would drop the params
141 * binding and no longer match the bind group. An explicit layout may carry
142 * bindings the shader ignores.
143 */
144function kernelLayout(device: GPUDevice, inputs: number): GPUBindGroupLayout {
145 const readOnly = (binding: number): GPUBindGroupLayoutEntry => ({
146 binding,
147 visibility: GPUShaderStage.COMPUTE,
148 buffer: { type: 'read-only-storage' },
149 });
150 return device.createBindGroupLayout({
151 entries: [
152 {
153 binding: 0,
154 visibility: GPUShaderStage.COMPUTE,
155 buffer: { type: 'storage' },
156 },
157 ...Array.from({ length: inputs }, (_, i) => readOnly(i + 1)),
158 readOnly(inputs + 1),
159 ],
160 });
161}
163async function makePipeline(
164 device: GPUDevice,
165 code: string,
166 label: string,
167 bindGroupLayout: GPUBindGroupLayout,
168): Promise<GPUComputePipeline> {
169 device.pushErrorScope('validation');
170 const module = device.createShaderModule({ code, label });
171 const info = await module.getCompilationInfo();
172 const errors = info.messages.filter((m) => m.type === 'error');
173 if (errors.length) {
174 throw new UnsupportedOnGpu(
175 `generated WGSL failed to compile for '${label}':\n` +
176 errors.map((e) => ` ${e.lineNum}:${e.linePos} ${e.message}`).join('\n') +
177 `\n--- shader ---\n${code}`,
178 );
179 }
180 const pipeline = await device.createComputePipelineAsync({
181 layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
182 compute: { module, entryPoint: 'main' },
183 label,
184 });
185 const err = await device.popErrorScope();
186 if (err) throw new UnsupportedOnGpu(`pipeline '${label}': ${err.message}`);
187 return pipeline;
188}
190/** A compiled .m step, ready to run on the GPU. */
191export class ModelPlan {
192 /** Scalar parameter names, in the order the params buffer expects them. */
193 readonly paramNames: string[];
195 #device: GPUDevice;
196 #sht: ShtPlan;
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 197 #deriv?: DerivPlan;
199 #owned: GPUBuffer[];
200 #paramBuf: GPUBuffer;
201 #paramData: Float32Array;
202 /** Public name -> buffer, for uploading initial state and reading results. */
203 #byName: Map<string, Slot>;
205 private constructor(init: {
206 device: GPUDevice;
207 sht: ShtPlan;
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 208 deriv?: DerivPlan;
210 byName: Map<string, Slot>;
211 owned: GPUBuffer[];
212 paramBuf: GPUBuffer;
213 paramData: Float32Array;
214 paramNames: string[];
215 }) {
216 this.#device = init.device;
217 this.#sht = init.sht;
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 218 this.#deriv = init.deriv;
220 this.#byName = init.byName;
221 this.#owned = init.owned;
222 this.#paramBuf = init.paramBuf;
223 this.#paramData = init.paramData;
224 this.paramNames = init.paramNames;
225 }
227 static async create(
228 device: GPUDevice,
229 sht: ShtPlan,
230 spec: PlanSpec,
231 host: HostBuffers,
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 232 /** Computes dtheta/dphi — only needed if the .m calls them. */
233 deriv?: DerivPlan,
235 const { fn } = spec;
237 const slots = new Map<string, Slot>();
238 const byName = new Map<string, Slot>();
239 const owned: GPUBuffer[] = [];
240 /** Scalars the .m computes from its parameters, by cName. */
241 const derivedScalars = new Map<string, { name: string; expr: IRExpr }>();
243 const alloc = (label: string, count: number): Slot => {
244 const buffer = makeBuffer(device, label, count);
245 owned.push(buffer);
246 return { buffer, count };
247 };
249 // Arguments, bound by what the function's signature declares. Array
250 // arguments come from the shared pool, so a value one function returns is
251 // the same buffer the next one reads. Scalar parameters share one small
252 // storage buffer, in signature order.
253 const paramNames: string[] = [];
254 const paramSlots = new Map<string, number>();
255 for (const p of fn.params) {
256 if (p.binding.kind === 'tensor') {
257 const count = p.binding.shape.reduce((x, y) => x * y, 1);
258 const slot = host.ensure(p.name, count);
259 slots.set(p.cName, slot);
260 byName.set(p.name, slot);
261 } else if (p.binding.kind === 'param') {
262 paramSlots.set(p.cName, paramNames.length);
263 paramNames.push(p.name);
264 }
265 // `const` arguments are exact in the IR and fold into the kernels.
266 }
267 const paramData = new Float32Array(Math.max(1, paramNames.length));
268 const paramBuf = device.createBuffer({
269 label: 'mgpu-params',
270 size: 4 * paramData.length,
271 usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
272 });
274 const ops: Op[] = [];
275 for (const stmt of fn.body) {
276 await planStatement(stmt);
277 }
279 // Feed declared outputs back into the argument buffers they replace.
280 fn.outputs.forEach((out, i) => {
281 const to = spec.feedback[i];
282 if (!to) return;
283 const src = slots.get(out.cName);
284 const dst = host.get(to);
285 if (!src) {
286 throw new UnsupportedOnGpu(
287 `'${fn.name}' declares the output '${out.name}' but never assigns it`,
288 );
289 }
290 if (!dst) throw new UnsupportedOnGpu(`'${to}' is not a host binding`);
291 if (src.count !== dst.count) {
292 throw new UnsupportedOnGpu(
293 `'${out.name}' (${src.count} elements) cannot feed ` +
294 `'${to}' (${dst.count})`,
295 );
296 }
297 ops.push({
298 kind: 'copy',
299 from: src.buffer,
300 to: dst.buffer,
301 bytes: 4 * src.count,
302 label: `${out.name} -> ${to}`,
303 });
304 });
306 return new ModelPlan({
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 307 device, sht, deriv, ops, byName, owned, paramBuf, paramData, paramNames,
310 async function planStatement(stmt: IRStmt): Promise<void> {
311 if (stmt.kind === 'ReturnFromFunction') return; // nothing follows it
312 if (stmt.kind === 'For') return planFor(stmt);
313 if (stmt.kind !== 'Assign') {
314 throw new UnsupportedOnGpu(
315 `a model function body may only contain assignments ` +
316 `(found '${stmt.kind}')`,
317 stmt.span,
318 );
319 }
320 if (!isNumeric(stmt.ty)) {
321 throw new UnsupportedOnGpu(
322 `'${stmt.name}' is not a numeric value`,
323 stmt.span,
324 );
325 }
326 if (!isTensor(stmt.ty)) {
327 // A scalar the model derives from its parameters (`us = a + b`). It
328 // gets no buffer and no dispatch: the kernels that read it bind it as
329 // a `let` in their prologue.
330 derivedScalars.set(stmt.cName, { name: stmt.name, expr: stmt.expr });
331 return;
332 }
333 const count = numel(stmt.ty);
335 // Reuse the destination buffer across steps: the same cName always maps
336 // to the same buffer, so a step allocates nothing.
337 let dest = slots.get(stmt.cName);
338 if (!dest) {
339 dest = alloc(`mgpu-${stmt.name}`, count);
340 slots.set(stmt.cName, dest);
341 } else if (dest.count !== count) {
342 throw new UnsupportedOnGpu(
343 `'${stmt.name}' changes size between assignments`,
344 stmt.span,
345 );
346 }
347 byName.set(stmt.name, dest);
349 const ext = externalCall(stmt);
350 if (ext) {
351 const argSlot = slots.get(ext.argCName);
352 if (!argSlot) {
353 throw new UnsupportedOnGpu(
354 `'${ext.name}' reads '${ext.argName}', which has no buffer`,
355 stmt.span,
356 );
357 }
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 358 const label = `${stmt.name} = ${ext.name}(${ext.argName})`;
359 if (ext.name === 'synth') {
360 ops.push({
361 kind: 'synth',
362 binding: sht.createSynthBinding(argSlot.buffer, dest.buffer),
363 label,
364 });
365 } else if (ext.name === 'analys') {
366 ops.push({
367 kind: 'analys',
368 binding: sht.createAnalysBinding(argSlot.buffer, dest.buffer),
369 label,
370 });
371 } else if (ext.name === 'dtheta' || ext.name === 'dphi') {
372 if (!deriv) {
373 throw new UnsupportedOnGpu(
374 `'${ext.name}' needs the surface's derivative transforms, ` +
375 `which this plan was not given`,
376 stmt.span,
377 );
378 }
379 ops.push(
380 ext.name === 'dtheta'
381 ? { kind: 'dtheta', binding: deriv.createDthetaBinding(argSlot.buffer, dest.buffer), label }
382 : { kind: 'dphi', binding: deriv.createDphiBinding(argSlot.buffer, dest.buffer), label },
383 );
384 } else {
385 throw new UnsupportedOnGpu(`unknown external op '${ext.name}'`, stmt.span);
386 }
388 }
390 // Element-wise kernel. Collect the distinct tensor operands and give
391 // them dense binding slots.
392 const tensors = new Map<string, number>();
393 collectTensorVars(stmt.expr, (cName) => {
394 if (!tensors.has(cName)) tensors.set(cName, tensors.size);
395 });
397 const label = `${stmt.name} = <${count} elements, element-wise>`;
398 const kernel = buildKernel(
399 stmt,
400 {
401 tensors,
402 params: paramSlots,
403 scalars: derivedScalars,
404 } satisfies KernelInputs,
405 count,
406 label,
407 );
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 408
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 409 const bindGroupLayout = kernelLayout(device, tensors.size);
410 const pipeline = await makePipeline(device, kernel.code, label, bindGroupLayout);
412 // WebGPU forbids aliasing a writable storage binding with another
413 // binding in the same group, so an in-place update (`u = u + 1`) writes
414 // to scratch and copies back. Element-wise kernels only ever touch
415 // their own index, so the copy is the only cost.
416 const aliased = tensors.has(stmt.cName);
417 const target = aliased ? alloc(`mgpu-${stmt.name}-scratch`, count) : dest;
419 const entries: GPUBindGroupEntry[] = [
420 { binding: 0, resource: { buffer: target.buffer } },
421 ];
422 for (const [cName, i] of tensors) {
423 const s = slots.get(cName);
424 if (!s) {
425 throw new UnsupportedOnGpu(
426 `'${stmt.name}' reads a value with no buffer`,
427 stmt.span,
428 );
429 }
430 entries.push({ binding: i + 1, resource: { buffer: s.buffer } });
431 }
432 entries.push({ binding: tensors.size + 1, resource: { buffer: paramBuf } });
434 ops.push({
435 kind: 'kernel',
436 pipeline,
437 bindGroup: device.createBindGroup({
438 layout: bindGroupLayout,
439 entries,
440 }),
441 count,
442 label,
443 copyBack: aliased
444 ? { from: target.buffer, to: dest.buffer, bytes: 4 * count }
445 : undefined,
446 });
447 }
449 /**
450 * Unroll a counted loop into the op sequence.
451 *
452 * A plan is a fixed list of GPU operations with no branching, which is what
453 * makes a timestep pure command recording. A `for` with compile-time-known
454 * bounds still fits that: it is the same body planned once per iteration.
455 * Nothing else changes — numbl gives a variable one cName for every
456 * assignment to it, so the buffer an iteration writes is the buffer the
457 * next one reads, which is exactly a loop-carried value.
458 *
459 * The loop variable gets no buffer either: it is bound as a derived scalar
460 * to this iteration's literal value, so a kernel that reads `k` folds the
461 * number in. The binding is overwritten per iteration, before that
462 * iteration's body is planned and its WGSL emitted.
463 */
464 async function planFor(stmt: For): Promise<void> {
465 const from = exactValue(stmt.start);
466 const to = exactValue(stmt.end);
467 if (from === undefined || to === undefined) {
468 throw new UnsupportedOnGpu(
469 `a 'for' loop is unrolled into the op sequence, so its bounds must ` +
470 `be known when the model is compiled — ` +
471 `${from === undefined ? 'the start' : 'the end'} of this one is a ` +
472 `runtime value. Use a whole number, or a count the app supplies ` +
473 `as a fixed argument (changing it recompiles).`,
474 stmt.span,
475 );
476 }
477 const trips = Math.floor((to - from) / stmt.step) + 1;
478 if (!Number.isFinite(trips)) {
479 throw new UnsupportedOnGpu(`'for ${stmt.varName}' has no finite length`, stmt.span);
480 }
481 if (trips > MAX_UNROLL) {
482 throw new UnsupportedOnGpu(
483 `'for ${stmt.varName}' would unroll to ${trips} iterations, over the ` +
484 `limit of ${MAX_UNROLL}. Every iteration is separate GPU work, so a ` +
485 `long loop compiles slowly and runs no faster than writing it out.`,
486 stmt.span,
487 );
488 }
489 for (let i = 0; i < trips; i++) {
490 const value = from + i * stmt.step;
491 derivedScalars.set(stmt.cVar, {
492 name: stmt.varName,
493 expr: {
494 kind: 'NumLit',
495 value,
496 ty: scalarDouble(
497 value > 0 ? 'positive' : value < 0 ? 'negative' : 'zero',
498 value,
499 ),
500 span: stmt.span,
501 },
502 });
503 for (const s of stmt.body) await planStatement(s);
504 }
505 }
506 }
508 /** Upload parameter values, in `paramNames` order. Cheap — call freely. */
509 setParams(values: Record<string, number>): void {
510 this.paramNames.forEach((name, i) => {
511 const v = values[name];
512 this.#paramData[i] = Number.isFinite(v) ? v : 0;
513 });
514 this.#device.queue.writeBuffer(
515 this.#paramBuf,
516 0,
517 this.#paramData as Float32Array<ArrayBuffer>,
518 );
519 }
521 /** Buffer holding the named value, or undefined if the .m never binds it. */
522 buffer(name: string): GPUBuffer | undefined {
523 return this.#byName.get(name)?.buffer;
524 }
526 elementCount(name: string): number | undefined {
527 return this.#byName.get(name)?.count;
528 }
530 /**
531 * Record `steps` timesteps. Synchronous: no awaits, no readback. All of the
532 * ops share one compute pass, which WebGPU executes in submission order
533 * with a barrier between dispatches.
534 */
535 encodeSteps(encoder: GPUCommandEncoder, steps: number): void {
536 for (let s = 0; s < steps; s++) {
537 let pass: GPUComputePassEncoder | null = null;
538 const inPass = (): GPUComputePassEncoder => {
539 if (!pass) pass = encoder.beginComputePass({ label: 'mgpu-step' });
540 return pass;
541 };
542 const endPass = (): void => {
543 if (pass) {
544 pass.end();
545 pass = null;
546 }
547 };
548 for (const op of this.#ops) {
549 switch (op.kind) {
550 case 'kernel': {
551 const p = inPass();
552 p.setPipeline(op.pipeline);
553 p.setBindGroup(0, op.bindGroup);
554 p.dispatchWorkgroups(Math.ceil(op.count / WORKGROUP_SIZE));
555 if (op.copyBack) {
556 endPass();
557 encoder.copyBufferToBuffer(
558 op.copyBack.from, 0, op.copyBack.to, 0, op.copyBack.bytes,
559 );
560 }
561 break;
562 }
563 case 'synth':
564 this.#shtInto(inPass(), op);
565 break;
566 case 'analys':
567 this.#shtInto(inPass(), op);
568 break;
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 569 case 'dtheta':
570 this.#derivInto(inPass(), op);
571 break;
572 case 'dphi':
573 this.#derivInto(inPass(), op);
574 break;
576 endPass();
577 encoder.copyBufferToBuffer(op.from, 0, op.to, 0, op.bytes);
578 break;
579 }
580 }
581 endPass();
582 }
583 }
585 #shtInto(pass: GPUComputePassEncoder, op: Op & { kind: 'synth' | 'analys' }): void {
586 if (op.kind === 'synth') this.#sht.encodeSynthInto(pass, op.binding);
587 else this.#sht.encodeAnalysInto(pass, op.binding);
588 }
8144287WIP: added code for Laplace-Beltrami operator evaluation on smooth genus-0 surfaceOwen Melia 590 #derivInto(pass: GPUComputePassEncoder, op: Op & { kind: 'dtheta' | 'dphi' }): void {
591 // planStatement already refused to plan a dtheta/dphi op without a
592 // DerivPlan, so #deriv is guaranteed set whenever an op of this kind exists.
593 if (op.kind === 'dtheta') this.#deriv!.encodeDthetaInto(pass, op.binding);
594 else this.#deriv!.encodeDphiInto(pass, op.binding);
595 }
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 597 /** Human-readable op sequence — what the .m actually compiled to. */
598 describe(): string[] {
599 return this.#ops.map((op) => `${op.kind.padEnd(7)} ${op.label}`);
600 }
602 destroy(): void {
603 for (const b of this.#owned) b.destroy();
604 this.#paramBuf.destroy();
605 this.#owned.length = 0;
606 }
607}
609/** `x = synth(y)` / `x = analys(y)` -> the call's name and argument. */
610function externalCall(
611 stmt: Assign,
612): { name: string; argCName: string; argName: string } | null {
613 const e = stmt.expr;
614 if (e.kind !== 'Call' || !EXTERNAL_OPS.has(e.name)) return null;
615 if (e.args.length !== 1 || e.args[0].kind !== 'Var') {
616 throw new UnsupportedOnGpu(
617 `'${e.name}' must be applied to a single variable`,
618 stmt.span,
619 );
620 }
621 const arg = e.args[0];
622 return { name: e.name, argCName: arg.cName, argName: arg.name };
623}
625function collectTensorVars(e: IRExpr, visit: (cName: string) => void): void {
626 const walk = (x: IRExpr): void => {
627 switch (x.kind) {
628 case 'Var':
629 if (isTensor(x.ty)) visit(x.cName);
630 return;
631 case 'Binary':
632 walk(x.left);
633 walk(x.right);
634 return;
635 case 'Unary':
636 walk(x.operand);
637 return;
638 case 'Call':
639 x.args.forEach(walk);
640 return;
641 default:
642 return;
643 }
644 };
645 walk(e);
646}