@johnhenry/math 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Shader.ts ADDED
@@ -0,0 +1,837 @@
1
+ /**
2
+ * Shader code generation for `Expr` trees: GLSL ES 3.0 and WGSL.
3
+ *
4
+ * Output is deterministic (no timestamps, ids or hash ordering): the same tree
5
+ * and options always give the same strings, so callers can golden-test them
6
+ * and cache compiled programs by source text.
7
+ */
8
+ import type { Expr } from "./Symbolic.ts";
9
+
10
+ export type ShaderDialect = "glsl" | "wgsl";
11
+
12
+ export interface ShaderOptions {
13
+ /**
14
+ * `false` (default): every value is a scalar `float` / `f32`.
15
+ * `true`: every value is a complex number held in a `vec2` (x = real,
16
+ * y = imaginary); the variable `i` (unless listed in `variables`) is the
17
+ * imaginary unit and the helper functions the expression needs are emitted once.
18
+ */
19
+ complex?: boolean;
20
+ /**
21
+ * Free variables the expression may mention; they are emitted as identifiers of the
22
+ * same name and must be in scope where the expression is pasted. Default
23
+ * `["x"]` (real) or `["z"]` (complex). A variable not listed is an error.
24
+ */
25
+ variables?: string[];
26
+ /** When set, `source` is a complete function `name(<variables>)` wrapping the expression. */
27
+ functionName?: string;
28
+ }
29
+
30
+ export interface ShaderResult {
31
+ /** The expression text, e.g. `sin(x) * 2.0`. Paste it where a float (or vec2) is wanted. */
32
+ expression: string;
33
+ /** Helper function definitions the expression calls, in dependency order; empty when none are needed. */
34
+ helpers: string;
35
+ /** Complete source: `helpers` followed by the wrapping function when `functionName` was given, else `helpers` and nothing more. */
36
+ source: string;
37
+ /** The free variables actually used, sorted. */
38
+ variables: string[];
39
+ }
40
+
41
+ export class ShaderUnsupportedError extends Error {
42
+ constructor(message: string) {
43
+ super(message);
44
+ this.name = "ShaderUnsupportedError";
45
+ }
46
+ }
47
+
48
+ const RESERVED = new Set([
49
+ // GLSL ES 3.0 keywords (incl. reserved-for-future) and builtin functions, WGSL keywords, reserved words and builtins
50
+ "NULL",
51
+ "Self",
52
+ "abs",
53
+ "abstract",
54
+ "acos",
55
+ "acosh",
56
+ "active",
57
+ "alias",
58
+ "alignas",
59
+ "alignof",
60
+ "all",
61
+ "any",
62
+ "array",
63
+ "arrayLength",
64
+ "as",
65
+ "asin",
66
+ "asinh",
67
+ "asm",
68
+ "asm_fragment",
69
+ "async",
70
+ "atan",
71
+ "atan2",
72
+ "atanh",
73
+ "atomic",
74
+ "attribute",
75
+ "auto",
76
+ "await",
77
+ "become",
78
+ "binding_array",
79
+ "bitcast",
80
+ "bool",
81
+ "break",
82
+ "buffer",
83
+ "bvec2",
84
+ "bvec3",
85
+ "bvec4",
86
+ "case",
87
+ "cast",
88
+ "catch",
89
+ "ceil",
90
+ "centroid",
91
+ "clamp",
92
+ "class",
93
+ "co_await",
94
+ "co_return",
95
+ "co_yield",
96
+ "coherent",
97
+ "column_major",
98
+ "common",
99
+ "compile",
100
+ "compile_fragment",
101
+ "concept",
102
+ "const",
103
+ "const_assert",
104
+ "const_cast",
105
+ "consteval",
106
+ "constexpr",
107
+ "constinit",
108
+ "continue",
109
+ "continuing",
110
+ "cos",
111
+ "cosh",
112
+ "countOneBits",
113
+ "crate",
114
+ "cross",
115
+ "dFdx",
116
+ "dFdy",
117
+ "debugger",
118
+ "decltype",
119
+ "default",
120
+ "degrees",
121
+ "delete",
122
+ "demote",
123
+ "demote_to_helper",
124
+ "determinant",
125
+ "diagnostic",
126
+ "discard",
127
+ "distance",
128
+ "do",
129
+ "dot",
130
+ "dvec2",
131
+ "dvec3",
132
+ "dvec4",
133
+ "dynamic_cast",
134
+ "else",
135
+ "enable",
136
+ "enum",
137
+ "equal",
138
+ "exp",
139
+ "exp2",
140
+ "explicit",
141
+ "export",
142
+ "extends",
143
+ "extern",
144
+ "external",
145
+ "f16",
146
+ "f32",
147
+ "faceforward",
148
+ "fallthrough",
149
+ "false",
150
+ "filter",
151
+ "final",
152
+ "finally",
153
+ "fixed",
154
+ "flat",
155
+ "float",
156
+ "floatBitsToInt",
157
+ "floatBitsToUint",
158
+ "floor",
159
+ "fma",
160
+ "fn",
161
+ "for",
162
+ "fract",
163
+ "friend",
164
+ "from",
165
+ "fvec2",
166
+ "fvec3",
167
+ "fvec4",
168
+ "fwidth",
169
+ "fxgroup",
170
+ "get",
171
+ "goto",
172
+ "greaterThan",
173
+ "greaterThanEqual",
174
+ "groupshared",
175
+ "half",
176
+ "highp",
177
+ "hvec2",
178
+ "hvec3",
179
+ "hvec4",
180
+ "i32",
181
+ "if",
182
+ "impl",
183
+ "implements",
184
+ "import",
185
+ "in",
186
+ "inline",
187
+ "inout",
188
+ "input",
189
+ "instanceof",
190
+ "int",
191
+ "intBitsToFloat",
192
+ "interface",
193
+ "invariant",
194
+ "inverse",
195
+ "inverseSqrt",
196
+ "inversesqrt",
197
+ "isampler1D",
198
+ "isampler1DArray",
199
+ "isampler2D",
200
+ "isampler2DArray",
201
+ "isampler2DMS",
202
+ "isampler2DMSArray",
203
+ "isampler2DRect",
204
+ "isampler3D",
205
+ "isamplerBuffer",
206
+ "isamplerCube",
207
+ "isinf",
208
+ "isnan",
209
+ "ivec2",
210
+ "ivec3",
211
+ "ivec4",
212
+ "layout",
213
+ "length",
214
+ "lessThan",
215
+ "lessThanEqual",
216
+ "let",
217
+ "log",
218
+ "log2",
219
+ "long",
220
+ "loop",
221
+ "lowp",
222
+ "macro",
223
+ "macro_rules",
224
+ "main",
225
+ "mat2",
226
+ "mat2x2",
227
+ "mat2x2f",
228
+ "mat2x3",
229
+ "mat2x4",
230
+ "mat3",
231
+ "mat3x2",
232
+ "mat3x3",
233
+ "mat3x3f",
234
+ "mat3x4",
235
+ "mat4",
236
+ "mat4x2",
237
+ "mat4x3",
238
+ "mat4x4",
239
+ "mat4x4f",
240
+ "match",
241
+ "matrixCompMult",
242
+ "max",
243
+ "mediump",
244
+ "meta",
245
+ "min",
246
+ "mix",
247
+ "mod",
248
+ "modf",
249
+ "module",
250
+ "move",
251
+ "mut",
252
+ "mutable",
253
+ "namespace",
254
+ "new",
255
+ "nil",
256
+ "noexcept",
257
+ "noinline",
258
+ "nointerpolation",
259
+ "noperspective",
260
+ "normalize",
261
+ "not",
262
+ "notEqual",
263
+ "null",
264
+ "nullptr",
265
+ "of",
266
+ "operator",
267
+ "out",
268
+ "outerProduct",
269
+ "output",
270
+ "override",
271
+ "packHalf2x16",
272
+ "packSnorm2x16",
273
+ "packUnorm2x16",
274
+ "package",
275
+ "packoffset",
276
+ "partition",
277
+ "pass",
278
+ "patch",
279
+ "pixelfragment",
280
+ "pow",
281
+ "precise",
282
+ "precision",
283
+ "premerge",
284
+ "priv",
285
+ "protected",
286
+ "ptr",
287
+ "pub",
288
+ "public",
289
+ "radians",
290
+ "readonly",
291
+ "ref",
292
+ "reflect",
293
+ "refract",
294
+ "regardless",
295
+ "register",
296
+ "reinterpret_cast",
297
+ "require",
298
+ "requires",
299
+ "resource",
300
+ "restrict",
301
+ "return",
302
+ "reverseBits",
303
+ "round",
304
+ "roundEven",
305
+ "sample",
306
+ "sampler",
307
+ "sampler1D",
308
+ "sampler1DArray",
309
+ "sampler1DArrayShadow",
310
+ "sampler1DShadow",
311
+ "sampler2D",
312
+ "sampler2DArray",
313
+ "sampler2DArrayShadow",
314
+ "sampler2DMS",
315
+ "sampler2DMSArray",
316
+ "sampler2DRect",
317
+ "sampler2DRectShadow",
318
+ "sampler2DShadow",
319
+ "sampler3D",
320
+ "samplerBuffer",
321
+ "samplerCube",
322
+ "samplerCubeShadow",
323
+ "sampler_comparison",
324
+ "saturate",
325
+ "select",
326
+ "self",
327
+ "set",
328
+ "shared",
329
+ "short",
330
+ "sign",
331
+ "sin",
332
+ "sinh",
333
+ "sizeof",
334
+ "smooth",
335
+ "smoothstep",
336
+ "snorm",
337
+ "sqrt",
338
+ "static",
339
+ "static_assert",
340
+ "static_cast",
341
+ "std",
342
+ "step",
343
+ "struct",
344
+ "subroutine",
345
+ "super",
346
+ "superp",
347
+ "switch",
348
+ "tan",
349
+ "tanh",
350
+ "target",
351
+ "template",
352
+ "texelFetch",
353
+ "texelFetchOffset",
354
+ "texture",
355
+ "textureGrad",
356
+ "textureGradOffset",
357
+ "textureLod",
358
+ "textureLodOffset",
359
+ "textureOffset",
360
+ "textureProj",
361
+ "textureProjGrad",
362
+ "textureProjGradOffset",
363
+ "textureProjLod",
364
+ "textureProjLodOffset",
365
+ "textureProjOffset",
366
+ "textureSize",
367
+ "texture_1d",
368
+ "texture_2d",
369
+ "texture_2d_array",
370
+ "texture_3d",
371
+ "texture_cube",
372
+ "texture_cube_array",
373
+ "texture_depth_2d",
374
+ "texture_multisampled_2d",
375
+ "texture_storage_1d",
376
+ "texture_storage_2d",
377
+ "texture_storage_2d_array",
378
+ "texture_storage_3d",
379
+ "this",
380
+ "thread_local",
381
+ "throw",
382
+ "trait",
383
+ "transpose",
384
+ "true",
385
+ "trunc",
386
+ "try",
387
+ "type",
388
+ "typedef",
389
+ "typeid",
390
+ "typename",
391
+ "typeof",
392
+ "u32",
393
+ "uint",
394
+ "uintBitsToFloat",
395
+ "uniform",
396
+ "union",
397
+ "unless",
398
+ "unorm",
399
+ "unpackHalf2x16",
400
+ "unpackSnorm2x16",
401
+ "unpackUnorm2x16",
402
+ "unsafe",
403
+ "unsigned",
404
+ "unsized",
405
+ "usampler1D",
406
+ "usampler1DArray",
407
+ "usampler2D",
408
+ "usampler2DArray",
409
+ "usampler2DMS",
410
+ "usampler2DMSArray",
411
+ "usampler2DRect",
412
+ "usampler3D",
413
+ "usamplerBuffer",
414
+ "usamplerCube",
415
+ "use",
416
+ "using",
417
+ "uvec2",
418
+ "uvec3",
419
+ "uvec4",
420
+ "var",
421
+ "varying",
422
+ "vec2",
423
+ "vec2f",
424
+ "vec2h",
425
+ "vec2i",
426
+ "vec2u",
427
+ "vec3",
428
+ "vec3f",
429
+ "vec3h",
430
+ "vec3i",
431
+ "vec3u",
432
+ "vec4",
433
+ "vec4f",
434
+ "vec4h",
435
+ "vec4i",
436
+ "vec4u",
437
+ "virtual",
438
+ "void",
439
+ "volatile",
440
+ "wgsl",
441
+ "where",
442
+ "while",
443
+ "with",
444
+ "writeonly",
445
+ "yield",
446
+ ]);
447
+
448
+ const LN10 = "2.302585092994046";
449
+
450
+ type Node = Expr;
451
+
452
+ interface Ctx {
453
+ d: ShaderDialect;
454
+ complex: boolean;
455
+ vars: Set<string>;
456
+ used: Set<string>;
457
+ helpers: Set<string>;
458
+ }
459
+
460
+ function lit(v: number, _c: Ctx): string {
461
+ if (!Number.isFinite(v)) throw new ShaderUnsupportedError(`constant ${v} has no shader literal`);
462
+ let s = String(v);
463
+ if (!/[.e]/i.test(s)) s += ".0";
464
+ else if (/^-?\d+e/i.test(s)) s = s.replace(/e/i, ".0e"); // 1e-7 -> 1.0e-7
465
+ // same float literal grammar in both dialects; negatives are parenthesised so "-" + "-2.0" never becomes "--2.0"
466
+ return v < 0 || Object.is(v, -0) ? `(${s})` : s;
467
+ }
468
+
469
+ function vec2(a: string, b: string, c: Ctx): string {
470
+ return c.d === "glsl" ? `vec2(${a}, ${b})` : `vec2<f32>(${a}, ${b})`;
471
+ }
472
+
473
+ function helper(c: Ctx, name: string): string {
474
+ c.helpers.add(name);
475
+ return `mx_${name}`;
476
+ }
477
+
478
+ function constValue(n: Node): number | undefined {
479
+ if (n.type === "const") return n.value;
480
+ if (n.type === "neg" && n.arg.type === "const") return -n.arg.value;
481
+ return undefined;
482
+ }
483
+
484
+ const CMP_OPS: Record<string, string> = { lt: "<", le: "<=", gt: ">", ge: ">=", eq: "==", ne: "!=" };
485
+
486
+ function cmpOp(op: string): string {
487
+ if (!Object.hasOwn(CMP_OPS, op)) throw new ShaderUnsupportedError(`unknown comparison operator "${op}"`);
488
+ return CMP_OPS[op] as string;
489
+ }
490
+
491
+ function boolean(n: Node, c: Ctx): string {
492
+ if (n.type === "cmp") {
493
+ const op = cmpOp(n.op);
494
+ return `${emit(n.left, c)} ${op} ${emit(n.right, c)}`;
495
+ }
496
+ return `${emit(n, c)} != 0.0`;
497
+ }
498
+
499
+ function unsupported(what: string): never {
500
+ throw new ShaderUnsupportedError(`${what} is not supported by the shader compiler`);
501
+ }
502
+
503
+ function emit(n: Node, c: Ctx): string {
504
+ switch (n.type) {
505
+ case "const":
506
+ return c.complex ? vec2(lit(n.value, c), "0.0", c) : lit(n.value, c);
507
+ case "var": {
508
+ if (c.complex && n.name === "i" && !c.vars.has("i")) return vec2("0.0", "1.0", c);
509
+ if (!c.vars.has(n.name)) throw new ShaderUnsupportedError(`variable "${n.name}" is not in the variables list`);
510
+ c.used.add(n.name);
511
+ return n.name;
512
+ }
513
+ case "neg":
514
+ return `(-${emit(n.arg, c)})`;
515
+ case "add":
516
+ return `(${emit(n.left, c)} + ${emit(n.right, c)})`;
517
+ case "sub":
518
+ return `(${emit(n.left, c)} - ${emit(n.right, c)})`;
519
+ case "mul":
520
+ return c.complex
521
+ ? `${helper(c, "cmul")}(${emit(n.left, c)}, ${emit(n.right, c)})`
522
+ : `(${emit(n.left, c)} * ${emit(n.right, c)})`;
523
+ case "div":
524
+ return c.complex
525
+ ? `${helper(c, "cdiv")}(${emit(n.left, c)}, ${emit(n.right, c)})`
526
+ : `(${emit(n.left, c)} / ${emit(n.right, c)})`;
527
+ case "pow":
528
+ return c.complex ? powComplex(n.base, n.exp, c) : powReal(n.base, n.exp, c);
529
+ case "func":
530
+ return c.complex ? funcComplex(n.name, emit(n.arg, c), c) : funcReal(n.name, emit(n.arg, c), c);
531
+ case "call2": {
532
+ if (c.complex) unsupported(`${n.name} over complex numbers`);
533
+ const a = emit(n.left, c);
534
+ const b = emit(n.right, c);
535
+ switch (n.name) {
536
+ case "atan2":
537
+ return c.d === "glsl" ? `atan(${a}, ${b})` : `atan2(${a}, ${b})`;
538
+ case "hypot":
539
+ return c.d === "glsl" ? `length(vec2(${a}, ${b}))` : `length(vec2<f32>(${a}, ${b}))`;
540
+ case "min":
541
+ case "max":
542
+ return `${n.name}(${a}, ${b})`;
543
+ default:
544
+ return unsupported(`${n.name}`);
545
+ }
546
+ }
547
+ case "cmp": {
548
+ if (c.complex) unsupported("comparison over complex numbers");
549
+ const t = boolean(n, c);
550
+ return c.d === "glsl" ? `(${t} ? 1.0 : 0.0)` : `select(0.0, 1.0, ${t})`;
551
+ }
552
+ case "piecewise": {
553
+ if (c.complex) unsupported("piecewise over complex numbers");
554
+ let out = emit(n.otherwise, c);
555
+ for (let k = n.branches.length - 1; k >= 0; k--) {
556
+ const br = n.branches[k] as { cond: Node; expr: Node };
557
+ const cond = boolean(br.cond, c);
558
+ const e = emit(br.expr, c);
559
+ out = c.d === "glsl" ? `(${cond} ? ${e} : ${out})` : `select(${out}, ${e}, ${cond})`;
560
+ }
561
+ return out;
562
+ }
563
+ case "sum":
564
+ return unsupported("sum");
565
+ case "product":
566
+ return unsupported("product");
567
+ }
568
+ }
569
+
570
+ function powReal(base: Node, exp: Node, c: Ctx): string {
571
+ const b = emit(base, c);
572
+ const k0 = constValue(exp);
573
+ if (k0 !== undefined && Number.isInteger(k0) && Math.abs(k0) <= 4) {
574
+ const k = k0;
575
+ if (k === 0) return "1.0";
576
+ if (k === 1) return b;
577
+ const prod = Array.from({ length: Math.abs(k) }, () => b).join(" * ");
578
+ return k > 0 ? `(${prod})` : `(1.0 / (${prod}))`;
579
+ }
580
+ if (k0 !== undefined && Number.isInteger(k0)) {
581
+ // pow() of a negative base is undefined in GLSL: split sign and magnitude for integer powers.
582
+ const k = k0;
583
+ const odd = Math.abs(k) % 2 === 1;
584
+ const mag = `pow(abs(${b}), ${lit(k, c)})`;
585
+ return odd ? `(sign(${b}) * ${mag})` : mag;
586
+ }
587
+ return `pow(${b}, ${emit(exp, c)})`;
588
+ }
589
+
590
+ function funcReal(name: string, a: string, c: Ctx): string {
591
+ const g = c.d === "glsl";
592
+ switch (name) {
593
+ case "sin":
594
+ case "cos":
595
+ case "tan":
596
+ case "exp":
597
+ case "sqrt":
598
+ case "asin":
599
+ case "acos":
600
+ case "atan":
601
+ case "sinh":
602
+ case "cosh":
603
+ case "tanh":
604
+ case "asinh":
605
+ case "acosh":
606
+ case "atanh":
607
+ case "abs":
608
+ case "sign":
609
+ case "floor":
610
+ case "ceil":
611
+ case "round":
612
+ case "trunc":
613
+ case "log2":
614
+ return `${name}(${a})`;
615
+ case "ln":
616
+ return `log(${a})`;
617
+ case "log10":
618
+ return `(log(${a}) / ${LN10})`;
619
+ case "cot":
620
+ return `(1.0 / tan(${a}))`;
621
+ case "sec":
622
+ return `(1.0 / cos(${a}))`;
623
+ case "csc":
624
+ return `(1.0 / sin(${a}))`;
625
+ case "coth":
626
+ return `(1.0 / tanh(${a}))`;
627
+ case "sech":
628
+ return `(1.0 / cosh(${a}))`;
629
+ case "csch":
630
+ return `(1.0 / sinh(${a}))`;
631
+ case "acot":
632
+ return `atan(1.0 / ${a})`;
633
+ case "asec":
634
+ return `acos(1.0 / ${a})`;
635
+ case "acsc":
636
+ return `asin(1.0 / ${a})`;
637
+ case "acoth":
638
+ return `atanh(1.0 / ${a})`;
639
+ case "asech":
640
+ return `acosh(1.0 / ${a})`;
641
+ case "acsch":
642
+ return `asinh(1.0 / ${a})`;
643
+ case "cbrt":
644
+ return `(sign(${a}) * pow(abs(${a}), 0.3333333333333333))`;
645
+ case "expm1":
646
+ return `(exp(${a}) - 1.0)`;
647
+ case "log1p":
648
+ return `log(1.0 + ${a})`;
649
+ case "sigmoid":
650
+ return `(1.0 / (1.0 + exp(-${a})))`;
651
+ case "relu":
652
+ return g ? `max(${a}, 0.0)` : `max(${a}, 0.0)`;
653
+ default:
654
+ return unsupported(`function ${name}`);
655
+ }
656
+ }
657
+
658
+ function powComplex(base: Node, exp: Node, c: Ctx): string {
659
+ const b = emit(base, c);
660
+ const k = constValue(exp);
661
+ if (k !== undefined && Number.isInteger(k) && Math.abs(k) <= 64) {
662
+ return `${helper(c, "cpowi")}(${b}, ${k})`;
663
+ }
664
+ return `${helper(c, "cpow")}(${b}, ${emit(exp, c)})`;
665
+ }
666
+
667
+ function funcComplex(name: string, a: string, c: Ctx): string {
668
+ switch (name) {
669
+ case "exp":
670
+ case "ln":
671
+ case "sqrt":
672
+ case "sin":
673
+ case "cos":
674
+ case "tan":
675
+ case "sinh":
676
+ case "cosh":
677
+ case "tanh":
678
+ case "abs":
679
+ return `${helper(c, `c${name}`)}(${a})`;
680
+ default:
681
+ return unsupported(`function ${name} over complex numbers`);
682
+ }
683
+ }
684
+
685
+ /** Helper bodies per dialect. Dependencies listed in HELPER_DEPS; emitted in HELPER_ORDER. */
686
+ const HELPER_ORDER = [
687
+ "cmul",
688
+ "cdiv",
689
+ "cexp",
690
+ "cln",
691
+ "cpow",
692
+ "cpowi",
693
+ "csqrt",
694
+ "csin",
695
+ "ccos",
696
+ "ctan",
697
+ "csinh",
698
+ "ccosh",
699
+ "ctanh",
700
+ "cabs",
701
+ ];
702
+ const HELPER_DEPS: Record<string, string[]> = {
703
+ cpow: ["cmul", "cexp", "cln"],
704
+ cpowi: ["cmul", "cdiv"],
705
+ ctan: ["cdiv", "csin", "ccos"],
706
+ ctanh: ["cdiv", "csinh", "ccosh"],
707
+ };
708
+
709
+ function helperSource(name: string, d: ShaderDialect): string {
710
+ const V = d === "glsl" ? "vec2" : "vec2<f32>";
711
+ const fn = (n: string, params: string, body: string) =>
712
+ d === "glsl"
713
+ ? `${V} mx_${n}(${params.replace(/(\w+): vec2|(\w+): int/g, (_m, a, b) => (a ? `vec2 ${a}` : `int ${b}`))}) {\n${body}\n}`
714
+ : `fn mx_${n}(${params.replace(/: vec2/g, ": vec2<f32>")}) -> ${V} {\n${body}\n}`;
715
+ const dec = (t: string, n: string, e: string) =>
716
+ d === "glsl" ? ` ${t} ${n} = ${e};` : ` let ${n}: ${t === "float" ? "f32" : t} = ${e};`;
717
+ const ret = (e: string) => ` return ${e};`;
718
+ const v = (a: string, b: string) => `${V}(${a}, ${b})`;
719
+ switch (name) {
720
+ case "cmul":
721
+ return fn("cmul", "a: vec2, b: vec2", ret(v("a.x * b.x - a.y * b.y", "a.x * b.y + a.y * b.x")));
722
+ case "cdiv":
723
+ return fn(
724
+ "cdiv",
725
+ "a: vec2, b: vec2",
726
+ [
727
+ dec("float", "m", "b.x * b.x + b.y * b.y"),
728
+ ret(v("(a.x * b.x + a.y * b.y) / m", "(a.y * b.x - a.x * b.y) / m")),
729
+ ].join("\n"),
730
+ );
731
+ case "cexp":
732
+ return fn("cexp", "z: vec2", ret(`exp(z.x) * ${v("cos(z.y)", "sin(z.y)")}`));
733
+ case "cln":
734
+ return fn(
735
+ "cln",
736
+ "z: vec2",
737
+ ret(v(d === "glsl" ? "log(length(z))" : "log(length(z))", d === "glsl" ? "atan(z.y, z.x)" : "atan2(z.y, z.x)")),
738
+ );
739
+ case "cpow":
740
+ return fn("cpow", "a: vec2, b: vec2", ret("mx_cexp(mx_cmul(b, mx_cln(a)))"));
741
+ case "cpowi": {
742
+ // exponentiation by squaring, bounded loop (|n| <= 64)
743
+ if (d === "glsl")
744
+ return `vec2 mx_cpowi(vec2 z, int n) {\n int k = n < 0 ? -n : n;\n vec2 r = vec2(1.0, 0.0);\n vec2 s = z;\n for (int j = 0; j < 7; j++) {\n if ((k & 1) == 1) r = mx_cmul(r, s);\n s = mx_cmul(s, s);\n k = k >> 1;\n }\n return n < 0 ? mx_cdiv(vec2(1.0, 0.0), r) : r;\n}`;
745
+ return `fn mx_cpowi(z: vec2<f32>, n: i32) -> vec2<f32> {\n var k: i32 = abs(n);\n var r: vec2<f32> = vec2<f32>(1.0, 0.0);\n var s: vec2<f32> = z;\n for (var j: i32 = 0; j < 7; j = j + 1) {\n if ((k & 1) == 1) { r = mx_cmul(r, s); }\n s = mx_cmul(s, s);\n k = k >> 1u;\n }\n if (n < 0) { return mx_cdiv(vec2<f32>(1.0, 0.0), r); }\n return r;\n}`;
746
+ }
747
+ case "csqrt":
748
+ return fn(
749
+ "csqrt",
750
+ "z: vec2",
751
+ [
752
+ dec("float", "r", "length(z)"),
753
+ ret(
754
+ v(
755
+ "sqrt(max(0.5 * (r + z.x), 0.0))",
756
+ d === "glsl"
757
+ ? "(z.y < 0.0 ? -1.0 : 1.0) * sqrt(max(0.5 * (r - z.x), 0.0))"
758
+ : "select(1.0, -1.0, z.y < 0.0) * sqrt(max(0.5 * (r - z.x), 0.0))",
759
+ ),
760
+ ),
761
+ ].join("\n"),
762
+ );
763
+ case "csin":
764
+ return fn("csin", "z: vec2", ret(v("sin(z.x) * cosh(z.y)", "cos(z.x) * sinh(z.y)")));
765
+ case "ccos":
766
+ return fn("ccos", "z: vec2", ret(v("cos(z.x) * cosh(z.y)", "-sin(z.x) * sinh(z.y)")));
767
+ case "ctan":
768
+ return fn("ctan", "z: vec2", ret("mx_cdiv(mx_csin(z), mx_ccos(z))"));
769
+ case "csinh":
770
+ return fn("csinh", "z: vec2", ret(v("sinh(z.x) * cos(z.y)", "cosh(z.x) * sin(z.y)")));
771
+ case "ccosh":
772
+ return fn("ccosh", "z: vec2", ret(v("cosh(z.x) * cos(z.y)", "sinh(z.x) * sin(z.y)")));
773
+ case "ctanh":
774
+ return fn("ctanh", "z: vec2", ret("mx_cdiv(mx_csinh(z), mx_ccosh(z))"));
775
+ case "cabs":
776
+ return fn("cabs", "z: vec2", ret(v("length(z)", "0.0")));
777
+ default:
778
+ throw new Error(`unknown helper ${name}`);
779
+ }
780
+ }
781
+
782
+ function safeIdentifier(v: string): boolean {
783
+ return (
784
+ /^[A-Za-z][A-Za-z0-9_]*$/.test(v) &&
785
+ !RESERVED.has(v) &&
786
+ !v.includes("__") &&
787
+ !v.startsWith("mx_") &&
788
+ !v.startsWith("gl_")
789
+ );
790
+ }
791
+
792
+ export function compileShader(expr: Expr, dialect: ShaderDialect, options: ShaderOptions = {}): ShaderResult {
793
+ const complex = options.complex === true;
794
+ const variables = options.variables ?? [complex ? "z" : "x"];
795
+ for (const v of variables) {
796
+ if (
797
+ !/^[A-Za-z][A-Za-z0-9_]*$/.test(v) ||
798
+ RESERVED.has(v) ||
799
+ v.includes("__") ||
800
+ v.startsWith("mx_") ||
801
+ v.startsWith("gl_")
802
+ )
803
+ throw new ShaderUnsupportedError(`variable name "${v}" is not a safe shader identifier`);
804
+ }
805
+ const fnName = options.functionName;
806
+ if (fnName !== undefined && (!safeIdentifier(fnName) || fnName === "main"))
807
+ throw new ShaderUnsupportedError(`function name "${fnName}" is not a safe shader identifier`);
808
+ const ctx: Ctx = { d: dialect, complex, vars: new Set(variables), used: new Set(), helpers: new Set() };
809
+ const expression = emit(expr, ctx);
810
+ // close over dependencies
811
+ const need = new Set(ctx.helpers);
812
+ for (let grew = true; grew; ) {
813
+ grew = false;
814
+ for (const h of [...need]) {
815
+ for (const dep of HELPER_DEPS[h] ?? []) {
816
+ if (!need.has(dep)) {
817
+ need.add(dep);
818
+ grew = true;
819
+ }
820
+ }
821
+ }
822
+ }
823
+ const helpers = HELPER_ORDER.filter((h) => need.has(h))
824
+ .map((h) => helperSource(h, dialect))
825
+ .join("\n\n");
826
+ let source = helpers;
827
+ if (options.functionName !== undefined) {
828
+ const T = complex ? (dialect === "glsl" ? "vec2" : "vec2<f32>") : dialect === "glsl" ? "float" : "f32";
829
+ const params = variables.map((v) => (dialect === "glsl" ? `${T} ${v}` : `${v}: ${T}`)).join(", ");
830
+ const fnText =
831
+ dialect === "glsl"
832
+ ? `${T} ${options.functionName}(${params}) {\n return ${expression};\n}`
833
+ : `fn ${options.functionName}(${params}) -> ${T} {\n return ${expression};\n}`;
834
+ source = helpers ? `${helpers}\n\n${fnText}` : fnText;
835
+ }
836
+ return { expression, helpers, source, variables: [...ctx.used].sort() };
837
+ }