2 * Type rules for the host-provided operations a model .m can call.
3 *
4 * numbl needs only each op's *shape contract* in order to lower a call site.
5 * It gets that from a `.mtoc2.js` workspace file — numbl's sanctioned
6 * extension point for a JS-defined builtin (see `mtoc2UserFunctionsByName` in
7 * numbl's LoweringContext). The file is evaluated in a bare CommonJS sandbox
8 * with no imports available, so `transfer` builds numbl `Type` objects as
9 * plain literals, with the element counts baked in by the generator below (a
10 * grid change recompiles anyway).
11 *
12 * Unlike dulcimer, the op set here is not fixed: the scene builder
13 * instantiates one op per (operation, object) pair, and this module turns
14 * whatever set the scene produced into workspace files. The actual
15 * implementations are the WGSL dispatches in src/mgpu/ops.ts.
16 *
17 * The `emit`/`cBody` exports exist only because the loader's contract
18 * requires them; we never emit C.
19 */
20import type { OpInstance } from './ops.ts';
22const numericType = (rows: number): string =>
23 `{ kind: "Numeric", elem: "double", isComplex: false, ` +
24 `dims: [{ kind: "exact", value: ${rows} }, { kind: "exact", value: 1 }], ` +
25 `shape: [${rows}, 1], sign: "unknown" }`;
27/** Source for one op's `.mtoc2.js`: fields in, field out, shapes checked. */
28function opSource(inst: Pick<OpInstance, 'name' | 'argCounts' | 'outCount'>): string {
29 const checks = inst.argCounts
30 .map((n, i) => {
31 return `
32 var a${i} = argTypes[${i}];
33 if (!a${i} || a${i}.kind !== "Numeric" || a${i}.isComplex) {
34 throw new Error("${inst.name}: argument ${i + 1} must be a real numeric array");
35 }
36 var s${i} = a${i}.shape;
37 if (!s${i} || s${i}.length !== 2 || s${i}[0] !== ${n} || s${i}[1] !== 1) {
38 throw new Error(
39 "${inst.name}: argument ${i + 1} must be a ${n}x1 field, not " +
40 (s${i} ? s${i}.join("x") : "unknown shape")
41 );
42 }`;
43 })
44 .join('\n');
45 return `
46exports.name = ${JSON.stringify(inst.name)};
48exports.transfer = function (argTypes, nargout) {
49 if (argTypes.length !== ${inst.argCounts.length}) {
50 throw new Error("${inst.name} takes ${inst.argCounts.length} argument(s), got " + argTypes.length);
51 }
52 if (nargout > 1) {
53 throw new Error("${inst.name} returns one value, but " + nargout + " were requested");
54 }
55${checks}
56 return [${numericType(inst.outCount)}];
57};
59// Never called: this project executes the IR on WebGPU and emits no C.
60exports.emit = function () {
61 throw new Error("${inst.name}: no C backend (this op runs on WebGPU)");
62};
63exports.cBody = function () {
64 return "";
65};
66`;
67}
69/** Workspace files that make the ops resolvable during lowering. */
70export function externalOpFiles(
71 instances: Pick<OpInstance, 'name' | 'argCounts' | 'outCount'>[],
72): { name: string; source: string }[] {
73 return instances.map((inst) => ({
74 name: `${inst.name}.mtoc2.js`,
75 source: opSource(inst),
76 }));
77}