| 863 | * jointly renamed by order of first appearance — so an equality and its swap |
| 864 | * (possibly with renamed variables) map to the same key. |
| 865 | */ |
| 866 | export function undirectedKey( |
| 867 | ce: ComputeEngine, |
| 868 | lhsW: MathJSON, |
| 869 | rhsW: MathJSON |
| 870 | ): string { |
| 871 | const side = (s: MathJSON): string => { |
| 872 | // Box in a throwaway scope: canonical boxing auto-declares the wildcard |
| 873 | // symbols, and those inferred types must not leak across entries. |
| 874 | ce.pushScope(); |
| 875 | try { |
| 876 | const b = ce.expr(s as never); |
| 877 | return JSON.stringify(b.json); |
| 878 | } catch { |
| 879 | return JSON.stringify(s); |
| 880 | } finally { |
| 881 | ce.popScope(); |
| 882 | } |
| 883 | }; |
| 884 | let a = side(lhsW); |
| 885 | let b = side(rhsW); |
| 886 | if (b < a) [a, b] = [b, a]; |
| 887 | const joined = a + '' + b; |
| 888 | const renames = new Map<string, string>(); |
| 889 | return joined.replace(/"(_{1,3}[^"\\]*)"/g, (_m, name: string) => { |
| 890 | if (!renames.has(name)) renames.set(name, `"_w${renames.size + 1}"`); |
| 891 | return renames.get(name)!; |
| 892 | }); |
| 893 | } |
| 894 | |
| 895 | // --------------------------------------------------------------------------- |