/** * Type rules for the host-provided operations a model .m can call. * * numbl needs only each op's *shape contract* in order to lower a call site. * It gets that from a `.mtoc2.js` workspace file — numbl's sanctioned * extension point for a JS-defined builtin (see `mtoc2UserFunctionsByName` in * numbl's LoweringContext). The file is evaluated in a bare CommonJS sandbox * with no imports available, so `transfer` builds numbl `Type` objects as * plain literals, with the element counts baked in by the generator below (a * grid change recompiles anyway). * * Unlike dulcimer, the op set here is not fixed: the scene builder * instantiates one op per (operation, object) pair, and this module turns * whatever set the scene produced into workspace files. The actual * implementations are the WGSL dispatches in src/mgpu/ops.ts. * * The `emit`/`cBody` exports exist only because the loader's contract * requires them; we never emit C. */ import type { OpInstance } from './ops.ts'; const numericType = (rows: number): string => `{ kind: "Numeric", elem: "double", isComplex: false, ` + `dims: [{ kind: "exact", value: ${rows} }, { kind: "exact", value: 1 }], ` + `shape: [${rows}, 1], sign: "unknown" }`; /** Source for one op's `.mtoc2.js`: fields in, field out, shapes checked. */ function opSource(inst: Pick): string { const checks = inst.argCounts .map((n, i) => { return ` var a${i} = argTypes[${i}]; if (!a${i} || a${i}.kind !== "Numeric" || a${i}.isComplex) { throw new Error("${inst.name}: argument ${i + 1} must be a real numeric array"); } var s${i} = a${i}.shape; if (!s${i} || s${i}.length !== 2 || s${i}[0] !== ${n} || s${i}[1] !== 1) { throw new Error( "${inst.name}: argument ${i + 1} must be a ${n}x1 field, not " + (s${i} ? s${i}.join("x") : "unknown shape") ); }`; }) .join('\n'); return ` exports.name = ${JSON.stringify(inst.name)}; exports.transfer = function (argTypes, nargout) { if (argTypes.length !== ${inst.argCounts.length}) { throw new Error("${inst.name} takes ${inst.argCounts.length} argument(s), got " + argTypes.length); } if (nargout > 1) { throw new Error("${inst.name} returns one value, but " + nargout + " were requested"); } ${checks} return [${numericType(inst.outCount)}]; }; // Never called: this project executes the IR on WebGPU and emits no C. exports.emit = function () { throw new Error("${inst.name}: no C backend (this op runs on WebGPU)"); }; exports.cBody = function () { return ""; }; `; } /** Workspace files that make the ops resolvable during lowering. */ export function externalOpFiles( instances: Pick[], ): { name: string; source: string }[] { return instances.map((inst) => ({ name: `${inst.name}.mtoc2.js`, source: opSource(inst), })); }