smiles-js 1.0.2 → 1.0.3
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/package.json +1 -1
- package/src/constructors.js +1 -1
- package/src/decompiler.js +7 -1
- package/src/decompiler.test.js +23 -23
- package/src/fragment.test.js +1 -1
- package/test-integration/acetaminophen.test.js +26 -22
- package/test-integration/adjuvant-analgesics.test.js +5 -2
- package/test-integration/celecoxib.test.js +34 -30
- package/test-integration/endocannabinoids.test.js +5 -2
- package/test-integration/hypertension-medication.test.js +5 -2
- package/test-integration/ketoprofen-debug.test.js +22 -19
- package/test-integration/local-anesthetics.test.js +5 -2
- package/test-integration/nabumetone-normalized.test.js +53 -0
- package/test-integration/nsaids-otc.test.js +5 -2
- package/test-integration/nsaids-prescription.test.js +5 -2
- package/test-integration/opioids.test.js +5 -2
- package/test-integration/steroids.test.js +5 -2
- package/test-integration/sulfonamide-debug.test.js +22 -20
- package/test-integration/telmisartan.test.js +46 -41
- package/test-integration/utils.js +14 -3
package/package.json
CHANGED
package/src/constructors.js
CHANGED
|
@@ -510,7 +510,7 @@ export function RawFragment(smilesString) {
|
|
|
510
510
|
};
|
|
511
511
|
},
|
|
512
512
|
toCode(varName = 'raw') {
|
|
513
|
-
return `const ${varName}1 = RawFragment(${JSON.stringify(smilesString)});`;
|
|
513
|
+
return `export const ${varName}1 = RawFragment(${JSON.stringify(smilesString)});`;
|
|
514
514
|
},
|
|
515
515
|
clone() {
|
|
516
516
|
return RawFragment(smilesString);
|
package/src/decompiler.js
CHANGED
|
@@ -806,6 +806,10 @@ function decompileNode(node, indent, nextVar) {
|
|
|
806
806
|
|
|
807
807
|
/**
|
|
808
808
|
* Main decompile dispatcher - public API
|
|
809
|
+
* @param {Object} node - AST node to decompile
|
|
810
|
+
* @param {Object} options - Options
|
|
811
|
+
* @param {number} options.indent - Indentation level (default 0)
|
|
812
|
+
* @param {string} options.varName - Variable name prefix (default 'v')
|
|
809
813
|
*/
|
|
810
814
|
export function decompile(node, options = {}) {
|
|
811
815
|
const { indent = 0, varName = 'v' } = options;
|
|
@@ -813,5 +817,7 @@ export function decompile(node, options = {}) {
|
|
|
813
817
|
const nextVar = createCounter(varName);
|
|
814
818
|
|
|
815
819
|
const { code } = decompileNode(node, indentStr, nextVar);
|
|
816
|
-
|
|
820
|
+
|
|
821
|
+
// Always use export const
|
|
822
|
+
return code.replace(/^(\s*)const /gm, '$1export const ');
|
|
817
823
|
}
|
package/src/decompiler.test.js
CHANGED
|
@@ -10,25 +10,25 @@ describe('Decompiler - Ring', () => {
|
|
|
10
10
|
test('decompiles simple ring', () => {
|
|
11
11
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
12
12
|
const code = decompile(benzene);
|
|
13
|
-
expect(code).toBe("const v1 = Ring({ atoms: 'c', size: 6 });");
|
|
13
|
+
expect(code).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test('decompiles ring with custom ring number', () => {
|
|
17
17
|
const ring = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
18
18
|
const code = decompile(ring);
|
|
19
|
-
expect(code).toBe("const v1 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });");
|
|
19
|
+
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test('decompiles ring with offset', () => {
|
|
23
23
|
const ring = Ring({ atoms: 'C', size: 6, offset: 2 });
|
|
24
24
|
const code = decompile(ring);
|
|
25
|
-
expect(code).toBe("const v1 = Ring({ atoms: 'C', size: 6, offset: 2 });");
|
|
25
|
+
expect(code).toBe("export const v1 = Ring({ atoms: 'C', size: 6, offset: 2 });");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
test('uses toCode() method', () => {
|
|
29
29
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
30
|
-
expect(benzene.toCode()).toBe("const ring1 = Ring({ atoms: 'c', size: 6 });");
|
|
31
|
-
expect(benzene.toCode('r')).toBe("const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
30
|
+
expect(benzene.toCode()).toBe("export const ring1 = Ring({ atoms: 'c', size: 6 });");
|
|
31
|
+
expect(benzene.toCode('r')).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
|
|
@@ -36,19 +36,19 @@ describe('Decompiler - Linear', () => {
|
|
|
36
36
|
test('decompiles simple linear chain', () => {
|
|
37
37
|
const propane = Linear(['C', 'C', 'C']);
|
|
38
38
|
const code = decompile(propane);
|
|
39
|
-
expect(code).toBe("const v1 = Linear(['C', 'C', 'C']);");
|
|
39
|
+
expect(code).toBe("export const v1 = Linear(['C', 'C', 'C']);");
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('decompiles linear with bonds', () => {
|
|
43
43
|
const ethene = Linear(['C', 'C'], ['=']);
|
|
44
44
|
const code = decompile(ethene);
|
|
45
|
-
expect(code).toBe("const v1 = Linear(['C', 'C'], ['=']);");
|
|
45
|
+
expect(code).toBe("export const v1 = Linear(['C', 'C'], ['=']);");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test('uses toCode() method', () => {
|
|
49
49
|
const propane = Linear(['C', 'C', 'C']);
|
|
50
|
-
expect(propane.toCode()).toBe("const linear1 = Linear(['C', 'C', 'C']);");
|
|
51
|
-
expect(propane.toCode('c')).toBe("const c1 = Linear(['C', 'C', 'C']);");
|
|
50
|
+
expect(propane.toCode()).toBe("export const linear1 = Linear(['C', 'C', 'C']);");
|
|
51
|
+
expect(propane.toCode('c')).toBe("export const c1 = Linear(['C', 'C', 'C']);");
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -59,9 +59,9 @@ describe('Decompiler - FusedRing', () => {
|
|
|
59
59
|
const fusedRing = ring1.fuse(ring2, 2);
|
|
60
60
|
|
|
61
61
|
const code = decompile(fusedRing);
|
|
62
|
-
expect(code).toBe(`const v1 = Ring({ atoms: 'C', size: 10 });
|
|
63
|
-
const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2 });
|
|
64
|
-
const v3 = v1.fuse(v2, 2);`);
|
|
62
|
+
expect(code).toBe(`export const v1 = Ring({ atoms: 'C', size: 10 });
|
|
63
|
+
export const v2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2 });
|
|
64
|
+
export const v3 = v1.fuse(v2, 2);`);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
test('uses toCode() method', () => {
|
|
@@ -70,9 +70,9 @@ const v3 = v1.fuse(v2, 2);`);
|
|
|
70
70
|
const fusedRing = ring1.fuse(ring2, 1);
|
|
71
71
|
|
|
72
72
|
const code = fusedRing.toCode();
|
|
73
|
-
expect(code).toBe(`const fusedRing1 = Ring({ atoms: 'C', size: 6 });
|
|
74
|
-
const fusedRing2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
75
|
-
const fusedRing3 = fusedRing1.fuse(fusedRing2, 1);`);
|
|
73
|
+
expect(code).toBe(`export const fusedRing1 = Ring({ atoms: 'C', size: 6 });
|
|
74
|
+
export const fusedRing2 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 1 });
|
|
75
|
+
export const fusedRing3 = fusedRing1.fuse(fusedRing2, 1);`);
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -83,9 +83,9 @@ describe('Decompiler - Molecule', () => {
|
|
|
83
83
|
const molecule = Molecule([propyl, benzene]);
|
|
84
84
|
|
|
85
85
|
const code = decompile(molecule);
|
|
86
|
-
expect(code).toBe(`const v1 = Linear(['C', 'C', 'C']);
|
|
87
|
-
const v2 = Ring({ atoms: 'c', size: 6 });
|
|
88
|
-
const v3 = Molecule([v1, v2]);`);
|
|
86
|
+
expect(code).toBe(`export const v1 = Linear(['C', 'C', 'C']);
|
|
87
|
+
export const v2 = Ring({ atoms: 'c', size: 6 });
|
|
88
|
+
export const v3 = Molecule([v1, v2]);`);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
test('uses toCode() method', () => {
|
|
@@ -94,9 +94,9 @@ const v3 = Molecule([v1, v2]);`);
|
|
|
94
94
|
const molecule = Molecule([propyl, benzene]);
|
|
95
95
|
|
|
96
96
|
const code = molecule.toCode();
|
|
97
|
-
expect(code).toBe(`const molecule1 = Linear(['C', 'C', 'C']);
|
|
98
|
-
const molecule2 = Ring({ atoms: 'c', size: 6 });
|
|
99
|
-
const molecule3 = Molecule([molecule1, molecule2]);`);
|
|
97
|
+
expect(code).toBe(`export const molecule1 = Linear(['C', 'C', 'C']);
|
|
98
|
+
export const molecule2 = Ring({ atoms: 'c', size: 6 });
|
|
99
|
+
export const molecule3 = Molecule([molecule1, molecule2]);`);
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -105,13 +105,13 @@ describe('Decompiler - Round-trip', () => {
|
|
|
105
105
|
const benzene = Ring({ atoms: 'c', size: 6 });
|
|
106
106
|
const code = benzene.toCode('r');
|
|
107
107
|
|
|
108
|
-
expect(code).toBe("const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
108
|
+
expect(code).toBe("export const r1 = Ring({ atoms: 'c', size: 6 });");
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
test('preserves structure through decompile', () => {
|
|
112
112
|
const propane = Linear(['C', 'C', 'C']);
|
|
113
113
|
const code = propane.toCode('p');
|
|
114
114
|
|
|
115
|
-
expect(code).toBe("const p1 = Linear(['C', 'C', 'C']);");
|
|
115
|
+
expect(code).toBe("export const p1 = Linear(['C', 'C', 'C']);");
|
|
116
116
|
});
|
|
117
117
|
});
|
package/src/fragment.test.js
CHANGED
|
@@ -63,6 +63,6 @@ describe('Fragment', () => {
|
|
|
63
63
|
|
|
64
64
|
test('toCode works on fragment', () => {
|
|
65
65
|
const benzene = Fragment('c1ccccc1');
|
|
66
|
-
expect(benzene.toCode('v')).toBe("const v1 = Ring({ atoms: 'c', size: 6 });");
|
|
66
|
+
expect(benzene.toCode('v')).toBe("export const v1 = Ring({ atoms: 'c', size: 6 });");
|
|
67
67
|
});
|
|
68
68
|
});
|
|
@@ -3,25 +3,26 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
const ACETAMINOPHEN_SMILES = 'CC(=O)Nc1ccc(O)cc1';
|
|
8
9
|
const PHENACETIN_SMILES = 'CC(=O)Nc1ccc(OCC)cc1';
|
|
9
10
|
|
|
10
|
-
const ACETAMINOPHEN_CODE = `const v1 = Linear(['C', 'C', 'N']);
|
|
11
|
-
const v2 = Linear(['O'], ['=']);
|
|
12
|
-
const v3 = v1.attach(v2, 2);
|
|
13
|
-
const v4 = Ring({ atoms: 'c', size: 6 });
|
|
14
|
-
const v5 = Linear(['O']);
|
|
15
|
-
const v6 = v4.attach(v5, 4);
|
|
16
|
-
const v7 = Molecule([v3, v6]);`;
|
|
17
|
-
|
|
18
|
-
const PHENACETIN_CODE = `const v1 = Linear(['C', 'C', 'N']);
|
|
19
|
-
const v2 = Linear(['O'], ['=']);
|
|
20
|
-
const v3 = v1.attach(v2, 2);
|
|
21
|
-
const v4 = Ring({ atoms: 'c', size: 6 });
|
|
22
|
-
const v5 = Linear(['O', 'C', 'C']);
|
|
23
|
-
const v6 = v4.attach(v5, 4);
|
|
24
|
-
const v7 = Molecule([v3, v6]);`;
|
|
11
|
+
const ACETAMINOPHEN_CODE = `export const v1 = Linear(['C', 'C', 'N']);
|
|
12
|
+
export const v2 = Linear(['O'], ['=']);
|
|
13
|
+
export const v3 = v1.attach(v2, 2);
|
|
14
|
+
export const v4 = Ring({ atoms: 'c', size: 6 });
|
|
15
|
+
export const v5 = Linear(['O']);
|
|
16
|
+
export const v6 = v4.attach(v5, 4);
|
|
17
|
+
export const v7 = Molecule([v3, v6]);`;
|
|
18
|
+
|
|
19
|
+
const PHENACETIN_CODE = `export const v1 = Linear(['C', 'C', 'N']);
|
|
20
|
+
export const v2 = Linear(['O'], ['=']);
|
|
21
|
+
export const v3 = v1.attach(v2, 2);
|
|
22
|
+
export const v4 = Ring({ atoms: 'c', size: 6 });
|
|
23
|
+
export const v5 = Linear(['O', 'C', 'C']);
|
|
24
|
+
export const v6 = v4.attach(v5, 4);
|
|
25
|
+
export const v7 = Molecule([v3, v6]);`;
|
|
25
26
|
|
|
26
27
|
describe('Acetaminophen Integration Test', () => {
|
|
27
28
|
test('parses acetaminophen', () => {
|
|
@@ -78,11 +79,12 @@ describe('Acetaminophen Integration Test', () => {
|
|
|
78
79
|
test('generated code is valid JavaScript', () => {
|
|
79
80
|
const ast = parse(ACETAMINOPHEN_SMILES);
|
|
80
81
|
const code = ast.toCode('v');
|
|
82
|
+
const executableCode = stripExports(code);
|
|
81
83
|
|
|
82
84
|
let factory;
|
|
83
85
|
expect(() => {
|
|
84
86
|
// eslint-disable-next-line no-new-func
|
|
85
|
-
factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
87
|
+
factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
86
88
|
}).not.toThrow();
|
|
87
89
|
expect(typeof factory).toBe('function');
|
|
88
90
|
});
|
|
@@ -90,12 +92,13 @@ describe('Acetaminophen Integration Test', () => {
|
|
|
90
92
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
91
93
|
const ast = parse(ACETAMINOPHEN_SMILES);
|
|
92
94
|
const code = ast.toCode('v');
|
|
95
|
+
const executableCode = stripExports(code);
|
|
93
96
|
|
|
94
|
-
const varMatch = code.match(/const (v\d+) = /g);
|
|
95
|
-
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/const (v\d+)/)[1] : 'v1';
|
|
97
|
+
const varMatch = code.match(/export const (v\d+) = /g);
|
|
98
|
+
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1] : 'v1';
|
|
96
99
|
|
|
97
100
|
// eslint-disable-next-line no-new-func
|
|
98
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
101
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${lastVar};`);
|
|
99
102
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
100
103
|
|
|
101
104
|
expect(reconstructed.smiles).toBe(ACETAMINOPHEN_SMILES);
|
|
@@ -157,12 +160,13 @@ describe('Phenacetin Integration Test', () => {
|
|
|
157
160
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
158
161
|
const ast = parse(PHENACETIN_SMILES);
|
|
159
162
|
const code = ast.toCode('v');
|
|
163
|
+
const executableCode = stripExports(code);
|
|
160
164
|
|
|
161
|
-
const varMatch = code.match(/const (v\d+) = /g);
|
|
162
|
-
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/const (v\d+)/)[1] : 'v1';
|
|
165
|
+
const varMatch = code.match(/export const (v\d+) = /g);
|
|
166
|
+
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1] : 'v1';
|
|
163
167
|
|
|
164
168
|
// eslint-disable-next-line no-new-func
|
|
165
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
169
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${lastVar};`);
|
|
166
170
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
167
171
|
|
|
168
172
|
expect(reconstructed.smiles).toBe(PHENACETIN_SMILES);
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
// Note: Parser normalizes some ring/branch patterns
|
|
8
9
|
|
|
@@ -56,19 +57,21 @@ describe('Adjuvant Analgesics', () => {
|
|
|
56
57
|
test('generated code is valid JavaScript', () => {
|
|
57
58
|
const ast = parse(data.smiles);
|
|
58
59
|
const code = ast.toCode('v');
|
|
60
|
+
const executableCode = stripExports(code);
|
|
59
61
|
|
|
60
62
|
expect(() => {
|
|
61
63
|
// eslint-disable-next-line no-new-func, no-new
|
|
62
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
64
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
63
65
|
}).not.toThrow();
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
test('codegen round-trip produces expected output', () => {
|
|
67
69
|
const ast = parse(data.smiles);
|
|
68
70
|
const code = ast.toCode('v');
|
|
71
|
+
const executableCode = stripExports(code);
|
|
69
72
|
|
|
70
73
|
// eslint-disable-next-line no-new-func
|
|
71
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
74
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
72
75
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
73
76
|
|
|
74
77
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -3,36 +3,37 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
const CELECOXIB_SMILES = 'CC1=CC=C(C=C1)C2=CC(=NN2C3=CC=C(C=C3)S(=O)(=O)N)C(F)(F)F';
|
|
8
9
|
|
|
9
|
-
const CELECOXIB_CODE = `const v1 = Linear(['C']);
|
|
10
|
-
const v2 = Ring({ atoms: 'C', size: 6 });
|
|
11
|
-
const v3 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
12
|
-
const v4 = v3.substitute(4, 'N');
|
|
13
|
-
const v5 = v4.substitute(5, 'N');
|
|
14
|
-
const v6 = Ring({ atoms: 'C', size: 6, ringNumber: 3 });
|
|
15
|
-
const v7 = v5.fuse(v6, 0);
|
|
16
|
-
const v8 = Linear(['C', 'F']);
|
|
17
|
-
const v9 = Linear(['F']);
|
|
18
|
-
const v10 = v8.attach(v9, 1);
|
|
19
|
-
const v11 = Linear(['F']);
|
|
20
|
-
const v12 = v10.attach(v11, 1);
|
|
21
|
-
const v13 = Molecule([v1, v2, v7, v12]);`;
|
|
22
|
-
|
|
23
|
-
const PYRAZOLE_PHENYL_CODE = `const v1 = Ring({ atoms: 'C', size: 5 });
|
|
24
|
-
const v2 = v1.substitute(4, 'N');
|
|
25
|
-
const v3 = v2.substitute(5, 'N');
|
|
26
|
-
const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
27
|
-
const v5 = v3.fuse(v4, 0);
|
|
28
|
-
const v6 = Linear(['C']);
|
|
29
|
-
const v7 = Molecule([v5, v6]);`;
|
|
30
|
-
|
|
31
|
-
const MINIMAL_SEQUENTIAL_CODE = `const v1 = Ring({ atoms: 'C', size: 4 });
|
|
32
|
-
const v2 = Ring({ atoms: 'C', size: 4, ringNumber: 2 });
|
|
33
|
-
const v3 = v1.fuse(v2, 0);
|
|
34
|
-
const v4 = Linear(['C']);
|
|
35
|
-
const v5 = Molecule([v3, v4]);`;
|
|
10
|
+
const CELECOXIB_CODE = `export const v1 = Linear(['C']);
|
|
11
|
+
export const v2 = Ring({ atoms: 'C', size: 6 });
|
|
12
|
+
export const v3 = Ring({ atoms: 'C', size: 5, ringNumber: 2 });
|
|
13
|
+
export const v4 = v3.substitute(4, 'N');
|
|
14
|
+
export const v5 = v4.substitute(5, 'N');
|
|
15
|
+
export const v6 = Ring({ atoms: 'C', size: 6, ringNumber: 3 });
|
|
16
|
+
export const v7 = v5.fuse(v6, 0);
|
|
17
|
+
export const v8 = Linear(['C', 'F']);
|
|
18
|
+
export const v9 = Linear(['F']);
|
|
19
|
+
export const v10 = v8.attach(v9, 1);
|
|
20
|
+
export const v11 = Linear(['F']);
|
|
21
|
+
export const v12 = v10.attach(v11, 1);
|
|
22
|
+
export const v13 = Molecule([v1, v2, v7, v12]);`;
|
|
23
|
+
|
|
24
|
+
const PYRAZOLE_PHENYL_CODE = `export const v1 = Ring({ atoms: 'C', size: 5 });
|
|
25
|
+
export const v2 = v1.substitute(4, 'N');
|
|
26
|
+
export const v3 = v2.substitute(5, 'N');
|
|
27
|
+
export const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
28
|
+
export const v5 = v3.fuse(v4, 0);
|
|
29
|
+
export const v6 = Linear(['C']);
|
|
30
|
+
export const v7 = Molecule([v5, v6]);`;
|
|
31
|
+
|
|
32
|
+
const MINIMAL_SEQUENTIAL_CODE = `export const v1 = Ring({ atoms: 'C', size: 4 });
|
|
33
|
+
export const v2 = Ring({ atoms: 'C', size: 4, ringNumber: 2 });
|
|
34
|
+
export const v3 = v1.fuse(v2, 0);
|
|
35
|
+
export const v4 = Linear(['C']);
|
|
36
|
+
export const v5 = Molecule([v3, v4]);`;
|
|
36
37
|
|
|
37
38
|
describe('Celecoxib Investigation', () => {
|
|
38
39
|
test('full molecule parse', () => {
|
|
@@ -158,9 +159,10 @@ describe('Celecoxib Code Round-Trip', () => {
|
|
|
158
159
|
test('generated code produces expected AST when executed', () => {
|
|
159
160
|
const ast = parse(CELECOXIB_SMILES);
|
|
160
161
|
const code = ast.toCode('v');
|
|
162
|
+
const executableCode = stripExports(code);
|
|
161
163
|
|
|
162
164
|
// eslint-disable-next-line no-new-func
|
|
163
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
165
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v13;`);
|
|
164
166
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
165
167
|
|
|
166
168
|
expect(reconstructed.type).toBe('molecule');
|
|
@@ -171,11 +173,12 @@ describe('Celecoxib Code Round-Trip', () => {
|
|
|
171
173
|
const smiles = 'C1=CC(=NN1C2=CC=CC=C2)C';
|
|
172
174
|
const ast = parse(smiles);
|
|
173
175
|
const code = ast.toCode('v');
|
|
176
|
+
const executableCode = stripExports(code);
|
|
174
177
|
|
|
175
178
|
expect(code).toBe(PYRAZOLE_PHENYL_CODE);
|
|
176
179
|
|
|
177
180
|
// eslint-disable-next-line no-new-func
|
|
178
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
181
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v7;`);
|
|
179
182
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
180
183
|
|
|
181
184
|
expect(reconstructed.type).toBe('molecule');
|
|
@@ -186,11 +189,12 @@ describe('Celecoxib Code Round-Trip', () => {
|
|
|
186
189
|
const smiles = 'C1CC(C1C2CCC2)C';
|
|
187
190
|
const ast = parse(smiles);
|
|
188
191
|
const code = ast.toCode('v');
|
|
192
|
+
const executableCode = stripExports(code);
|
|
189
193
|
|
|
190
194
|
expect(code).toBe(MINIMAL_SEQUENTIAL_CODE);
|
|
191
195
|
|
|
192
196
|
// eslint-disable-next-line no-new-func
|
|
193
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
197
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v5;`);
|
|
194
198
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
195
199
|
|
|
196
200
|
expect(reconstructed.type).toBe('molecule');
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
// Note: Many cannabinoid SMILES are too complex for current parser
|
|
8
9
|
// Only PEA works correctly
|
|
@@ -28,19 +29,21 @@ describe('Endocannabinoids', () => {
|
|
|
28
29
|
test('generated code is valid JavaScript', () => {
|
|
29
30
|
const ast = parse(data.smiles);
|
|
30
31
|
const code = ast.toCode('v');
|
|
32
|
+
const executableCode = stripExports(code);
|
|
31
33
|
|
|
32
34
|
expect(() => {
|
|
33
35
|
// eslint-disable-next-line no-new-func, no-new
|
|
34
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
36
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
35
37
|
}).not.toThrow();
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
test('codegen round-trip produces expected output', () => {
|
|
39
41
|
const ast = parse(data.smiles);
|
|
40
42
|
const code = ast.toCode('v');
|
|
43
|
+
const executableCode = stripExports(code);
|
|
41
44
|
|
|
42
45
|
// eslint-disable-next-line no-new-func
|
|
43
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
46
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
44
47
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
45
48
|
|
|
46
49
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
// Telmisartan is tested separately in telmisartan.test.js
|
|
8
9
|
// Note: Parser normalizes para-substituted benzene patterns
|
|
@@ -45,19 +46,21 @@ describe('Hypertension Medications', () => {
|
|
|
45
46
|
test('generated code is valid JavaScript', () => {
|
|
46
47
|
const ast = parse(data.smiles);
|
|
47
48
|
const code = ast.toCode('v');
|
|
49
|
+
const executableCode = stripExports(code);
|
|
48
50
|
|
|
49
51
|
expect(() => {
|
|
50
52
|
// eslint-disable-next-line no-new-func, no-new
|
|
51
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
53
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
52
54
|
}).not.toThrow();
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
test('codegen round-trip produces expected output', () => {
|
|
56
58
|
const ast = parse(data.smiles);
|
|
57
59
|
const code = ast.toCode('v');
|
|
60
|
+
const executableCode = stripExports(code);
|
|
58
61
|
|
|
59
62
|
// eslint-disable-next-line no-new-func
|
|
60
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
63
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
61
64
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
62
65
|
|
|
63
66
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -3,25 +3,26 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
|
-
const KETOPROFEN_CODE = `const v1 = Linear(['C', 'C', 'C', 'O']);
|
|
8
|
-
const v2 = Ring({ atoms: 'c', size: 6 });
|
|
9
|
-
const v3 = Linear(['C']);
|
|
10
|
-
const v4 = Linear(['O'], ['=']);
|
|
11
|
-
const v5 = v3.attach(v4, 1);
|
|
12
|
-
const v6 = Ring({ atoms: 'c', size: 6, ringNumber: 2 });
|
|
13
|
-
const v7 = Molecule([v2, v5, v6]);
|
|
14
|
-
const v8 = v1.attach(v7, 2);
|
|
15
|
-
const v9 = Linear(['O'], ['=']);
|
|
16
|
-
const v10 = v8.attach(v9, 3);`;
|
|
8
|
+
const KETOPROFEN_CODE = `export const v1 = Linear(['C', 'C', 'C', 'O']);
|
|
9
|
+
export const v2 = Ring({ atoms: 'c', size: 6 });
|
|
10
|
+
export const v3 = Linear(['C']);
|
|
11
|
+
export const v4 = Linear(['O'], ['=']);
|
|
12
|
+
export const v5 = v3.attach(v4, 1);
|
|
13
|
+
export const v6 = Ring({ atoms: 'c', size: 6, ringNumber: 2 });
|
|
14
|
+
export const v7 = Molecule([v2, v5, v6]);
|
|
15
|
+
export const v8 = v1.attach(v7, 2);
|
|
16
|
+
export const v9 = Linear(['O'], ['=']);
|
|
17
|
+
export const v10 = v8.attach(v9, 3);`;
|
|
17
18
|
|
|
18
|
-
const BIPHENYL_CODE = `const v1 = Linear(['C', 'C', 'C', 'O']);
|
|
19
|
-
const v2 = Ring({ atoms: 'c', size: 6 });
|
|
20
|
-
const v3 = Ring({ atoms: 'c', size: 6, ringNumber: 2 });
|
|
21
|
-
const v4 = Molecule([v2, v3]);
|
|
22
|
-
const v5 = v1.attach(v4, 2);
|
|
23
|
-
const v6 = Linear(['O'], ['=']);
|
|
24
|
-
const v7 = v5.attach(v6, 3);`;
|
|
19
|
+
const BIPHENYL_CODE = `export const v1 = Linear(['C', 'C', 'C', 'O']);
|
|
20
|
+
export const v2 = Ring({ atoms: 'c', size: 6 });
|
|
21
|
+
export const v3 = Ring({ atoms: 'c', size: 6, ringNumber: 2 });
|
|
22
|
+
export const v4 = Molecule([v2, v3]);
|
|
23
|
+
export const v5 = v1.attach(v4, 2);
|
|
24
|
+
export const v6 = Linear(['O'], ['=']);
|
|
25
|
+
export const v7 = v5.attach(v6, 3);`;
|
|
25
26
|
|
|
26
27
|
describe('Ketoprofen Debug', () => {
|
|
27
28
|
test('analyze structure', () => {
|
|
@@ -49,9 +50,10 @@ describe('Ketoprofen Code Round-Trip', () => {
|
|
|
49
50
|
const smiles = 'CC(c1cccc(c1)C(=O)c2ccccc2)C(=O)O';
|
|
50
51
|
const ast = parse(smiles);
|
|
51
52
|
const code = ast.toCode('v');
|
|
53
|
+
const executableCode = stripExports(code);
|
|
52
54
|
|
|
53
55
|
// eslint-disable-next-line no-new-func
|
|
54
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
56
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v10;`);
|
|
55
57
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
56
58
|
|
|
57
59
|
expect(reconstructed.type).toBe('linear');
|
|
@@ -69,9 +71,10 @@ describe('Ketoprofen Code Round-Trip', () => {
|
|
|
69
71
|
const smiles = 'CC(c1ccccc1c2ccccc2)C(=O)O';
|
|
70
72
|
const ast = parse(smiles);
|
|
71
73
|
const code = ast.toCode('v');
|
|
74
|
+
const executableCode = stripExports(code);
|
|
72
75
|
|
|
73
76
|
// eslint-disable-next-line no-new-func
|
|
74
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
77
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v7;`);
|
|
75
78
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
76
79
|
|
|
77
80
|
expect(reconstructed.type).toBe('linear');
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
// Note: Parser normalizes para-substituted benzene patterns
|
|
8
9
|
|
|
@@ -68,19 +69,21 @@ describe('Local Anesthetics', () => {
|
|
|
68
69
|
test('generated code is valid JavaScript', () => {
|
|
69
70
|
const ast = parse(data.smiles);
|
|
70
71
|
const code = ast.toCode('v');
|
|
72
|
+
const executableCode = stripExports(code);
|
|
71
73
|
|
|
72
74
|
expect(() => {
|
|
73
75
|
// eslint-disable-next-line no-new-func, no-new
|
|
74
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
76
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
75
77
|
}).not.toThrow();
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test('codegen round-trip produces expected output', () => {
|
|
79
81
|
const ast = parse(data.smiles);
|
|
80
82
|
const code = ast.toCode('v');
|
|
83
|
+
const executableCode = stripExports(code);
|
|
81
84
|
|
|
82
85
|
// eslint-disable-next-line no-new-func
|
|
83
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
86
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
84
87
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
85
88
|
|
|
86
89
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { Fragment } from '../src/fragment.js';
|
|
3
|
+
import { parse } from '../src/parser.js';
|
|
4
|
+
import {
|
|
5
|
+
Ring, Linear, Molecule,
|
|
6
|
+
} from '../src/constructors.js';
|
|
7
|
+
import { stripExports } from './utils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test case for Nabumetone that demonstrates full closed-loop round-trip:
|
|
11
|
+
* SMILES -> parse -> toCode() -> execute code -> SMILES (same as input)
|
|
12
|
+
*/
|
|
13
|
+
describe('Nabumetone Normalized', () => {
|
|
14
|
+
const NABUMETONE_SMILES = 'c1cc2ccc(OC)cc2cc1CCC(=O)C';
|
|
15
|
+
|
|
16
|
+
const EXPECTED_CODE = `export const v1 = Ring({ atoms: 'c', size: 6 });
|
|
17
|
+
export const v2 = Ring({ atoms: 'c', size: 6, ringNumber: 2, offset: 2 });
|
|
18
|
+
export const v3 = Linear(['O', 'C']);
|
|
19
|
+
export const v4 = v2.attach(v3, 4);
|
|
20
|
+
export const v5 = v1.fuse(v4, 2);
|
|
21
|
+
export const v6 = Linear(['C', 'C', 'C', 'C']);
|
|
22
|
+
export const v7 = Linear(['O'], ['=']);
|
|
23
|
+
export const v8 = v6.attach(v7, 3);
|
|
24
|
+
export const v9 = Molecule([v5, v8]);`;
|
|
25
|
+
|
|
26
|
+
test('Fragment parses normalized nabumetone SMILES', () => {
|
|
27
|
+
const nabumetone = Fragment(NABUMETONE_SMILES);
|
|
28
|
+
expect(nabumetone.smiles).toBe(NABUMETONE_SMILES);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('toCode generates expected code', () => {
|
|
32
|
+
const ast = parse(NABUMETONE_SMILES);
|
|
33
|
+
const code = ast.toCode('v');
|
|
34
|
+
expect(code).toBe(EXPECTED_CODE);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('CLOSED LOOP: SMILES -> toCode() -> execute -> same SMILES', () => {
|
|
38
|
+
// Parse the SMILES
|
|
39
|
+
const ast = parse(NABUMETONE_SMILES);
|
|
40
|
+
|
|
41
|
+
// Generate code
|
|
42
|
+
const code = ast.toCode('v');
|
|
43
|
+
|
|
44
|
+
// Execute the generated code
|
|
45
|
+
const executableCode = stripExports(code);
|
|
46
|
+
// eslint-disable-next-line no-new-func
|
|
47
|
+
const factory = new Function('Ring', 'Linear', 'Molecule', `${executableCode}\nreturn v9;`);
|
|
48
|
+
const reconstructed = factory(Ring, Linear, Molecule);
|
|
49
|
+
|
|
50
|
+
// Verify we get back the original SMILES
|
|
51
|
+
expect(reconstructed.smiles).toBe(NABUMETONE_SMILES);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
const MOLECULES = {
|
|
8
9
|
Aspirin: {
|
|
@@ -50,19 +51,21 @@ describe('OTC NSAIDs', () => {
|
|
|
50
51
|
test('generated code is valid JavaScript', () => {
|
|
51
52
|
const ast = parse(data.smiles);
|
|
52
53
|
const code = ast.toCode('v');
|
|
54
|
+
const executableCode = stripExports(code);
|
|
53
55
|
|
|
54
56
|
expect(() => {
|
|
55
57
|
// eslint-disable-next-line no-new-func, no-new
|
|
56
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
58
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
57
59
|
}).not.toThrow();
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
test('codegen round-trip produces expected output', () => {
|
|
61
63
|
const ast = parse(data.smiles);
|
|
62
64
|
const code = ast.toCode('v');
|
|
65
|
+
const executableCode = stripExports(code);
|
|
63
66
|
|
|
64
67
|
// eslint-disable-next-line no-new-func
|
|
65
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
68
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
66
69
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
67
70
|
|
|
68
71
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
// Note: Some complex SMILES don't round-trip correctly due to parser limitations
|
|
8
9
|
// Using parser output for now - TODO: fix parser for complex ring systems
|
|
@@ -75,19 +76,21 @@ describe('Prescription NSAIDs', () => {
|
|
|
75
76
|
test('generated code is valid JavaScript', () => {
|
|
76
77
|
const ast = parse(data.smiles);
|
|
77
78
|
const code = ast.toCode('v');
|
|
79
|
+
const executableCode = stripExports(code);
|
|
78
80
|
|
|
79
81
|
expect(() => {
|
|
80
82
|
// eslint-disable-next-line no-new-func, no-new
|
|
81
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
83
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
82
84
|
}).not.toThrow();
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
test('codegen round-trip produces expected output', () => {
|
|
86
88
|
const ast = parse(data.smiles);
|
|
87
89
|
const code = ast.toCode('v');
|
|
90
|
+
const executableCode = stripExports(code);
|
|
88
91
|
|
|
89
92
|
// eslint-disable-next-line no-new-func
|
|
90
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
93
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
91
94
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
92
95
|
|
|
93
96
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
// Note: Some complex opioid SMILES don't round-trip correctly due to parser limitations
|
|
8
9
|
// Parser output used where possible - some molecules are unstable
|
|
@@ -49,19 +50,21 @@ describe('Opioids', () => {
|
|
|
49
50
|
test('generated code is valid JavaScript', () => {
|
|
50
51
|
const ast = parse(data.smiles);
|
|
51
52
|
const code = ast.toCode('v');
|
|
53
|
+
const executableCode = stripExports(code);
|
|
52
54
|
|
|
53
55
|
expect(() => {
|
|
54
56
|
// eslint-disable-next-line no-new-func, no-new
|
|
55
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
57
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
56
58
|
}).not.toThrow();
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
test('codegen round-trip produces expected output', () => {
|
|
60
62
|
const ast = parse(data.smiles);
|
|
61
63
|
const code = ast.toCode('v');
|
|
64
|
+
const executableCode = stripExports(code);
|
|
62
65
|
|
|
63
66
|
// eslint-disable-next-line no-new-func
|
|
64
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
67
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
65
68
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
66
69
|
|
|
67
70
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
const MOLECULES = {
|
|
8
9
|
Cortisone: {
|
|
@@ -60,19 +61,21 @@ describe('Steroids', () => {
|
|
|
60
61
|
test('generated code is valid JavaScript', () => {
|
|
61
62
|
const ast = parse(data.smiles);
|
|
62
63
|
const code = ast.toCode('v');
|
|
64
|
+
const executableCode = stripExports(code);
|
|
63
65
|
|
|
64
66
|
expect(() => {
|
|
65
67
|
// eslint-disable-next-line no-new-func, no-new
|
|
66
|
-
new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
68
|
+
new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
67
69
|
}).not.toThrow();
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
test('codegen round-trip produces expected output', () => {
|
|
71
73
|
const ast = parse(data.smiles);
|
|
72
74
|
const code = ast.toCode('v');
|
|
75
|
+
const executableCode = stripExports(code);
|
|
73
76
|
|
|
74
77
|
// eslint-disable-next-line no-new-func
|
|
75
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
78
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${data.lastVar};`);
|
|
76
79
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
77
80
|
|
|
78
81
|
expect(reconstructed.type).toBe(data.expectedType);
|
|
@@ -4,25 +4,25 @@ import {
|
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
6
|
|
|
7
|
-
const SIMPLE_SULFONAMIDE_CODE = `const v1 = Ring({ atoms: 'C', size: 6 });
|
|
8
|
-
const v2 = Linear(['S', 'N']);
|
|
9
|
-
const v3 = Linear(['O'], ['=']);
|
|
10
|
-
const v4 = v2.attach(v3, 1);
|
|
11
|
-
const v5 = Linear(['O'], ['=']);
|
|
12
|
-
const v6 = v4.attach(v5, 1);
|
|
13
|
-
const v7 = Molecule([v1, v6]);`;
|
|
7
|
+
const SIMPLE_SULFONAMIDE_CODE = `export const v1 = Ring({ atoms: 'C', size: 6 });
|
|
8
|
+
export const v2 = Linear(['S', 'N']);
|
|
9
|
+
export const v3 = Linear(['O'], ['=']);
|
|
10
|
+
export const v4 = v2.attach(v3, 1);
|
|
11
|
+
export const v5 = Linear(['O'], ['=']);
|
|
12
|
+
export const v6 = v4.attach(v5, 1);
|
|
13
|
+
export const v7 = Molecule([v1, v6]);`;
|
|
14
14
|
|
|
15
|
-
const PYRAZOLE_SULFONAMIDE_CODE = `const v1 = Ring({ atoms: 'C', size: 5 });
|
|
16
|
-
const v2 = v1.substitute(4, 'N');
|
|
17
|
-
const v3 = v2.substitute(5, 'N');
|
|
18
|
-
const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
19
|
-
const v5 = v3.fuse(v4, 0);
|
|
20
|
-
const v6 = Linear(['C', 'F']);
|
|
21
|
-
const v7 = Linear(['F']);
|
|
22
|
-
const v8 = v6.attach(v7, 1);
|
|
23
|
-
const v9 = Linear(['F']);
|
|
24
|
-
const v10 = v8.attach(v9, 1);
|
|
25
|
-
const v11 = Molecule([v5, v10]);`;
|
|
15
|
+
const PYRAZOLE_SULFONAMIDE_CODE = `export const v1 = Ring({ atoms: 'C', size: 5 });
|
|
16
|
+
export const v2 = v1.substitute(4, 'N');
|
|
17
|
+
export const v3 = v2.substitute(5, 'N');
|
|
18
|
+
export const v4 = Ring({ atoms: 'C', size: 6, ringNumber: 2 });
|
|
19
|
+
export const v5 = v3.fuse(v4, 0);
|
|
20
|
+
export const v6 = Linear(['C', 'F']);
|
|
21
|
+
export const v7 = Linear(['F']);
|
|
22
|
+
export const v8 = v6.attach(v7, 1);
|
|
23
|
+
export const v9 = Linear(['F']);
|
|
24
|
+
export const v10 = v8.attach(v9, 1);
|
|
25
|
+
export const v11 = Molecule([v5, v10]);`;
|
|
26
26
|
|
|
27
27
|
describe('Sulfonamide Debug', () => {
|
|
28
28
|
test('simple sulfonamide', () => {
|
|
@@ -50,9 +50,10 @@ describe('Sulfonamide Code Round-Trip', () => {
|
|
|
50
50
|
const smiles = 'C1=CC=C(C=C1)S(=O)(=O)N';
|
|
51
51
|
const ast = parse(smiles);
|
|
52
52
|
const code = ast.toCode('v');
|
|
53
|
+
const executableCode = code.replace(/^export /gm, '');
|
|
53
54
|
|
|
54
55
|
// eslint-disable-next-line no-new-func
|
|
55
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
56
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v7;`);
|
|
56
57
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
57
58
|
|
|
58
59
|
expect(reconstructed.type).toBe('molecule');
|
|
@@ -70,9 +71,10 @@ describe('Sulfonamide Code Round-Trip', () => {
|
|
|
70
71
|
const smiles = 'C1=CC(=NN1C2=CC=C(C=C2)S(=O)(=O)N)C(F)(F)F';
|
|
71
72
|
const ast = parse(smiles);
|
|
72
73
|
const code = ast.toCode('v');
|
|
74
|
+
const executableCode = code.replace(/^export /gm, '');
|
|
73
75
|
|
|
74
76
|
// eslint-disable-next-line no-new-func
|
|
75
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
77
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v11;`);
|
|
76
78
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
77
79
|
|
|
78
80
|
expect(reconstructed.type).toBe('molecule');
|
|
@@ -3,6 +3,7 @@ import { parse } from '../src/parser.js';
|
|
|
3
3
|
import {
|
|
4
4
|
Ring, Linear, FusedRing, Molecule,
|
|
5
5
|
} from '../src/constructors.js';
|
|
6
|
+
import { stripExports } from './utils.js';
|
|
6
7
|
|
|
7
8
|
const TELMISARTAN_SMILES = 'CCCC1=NC2=C(C=C(C=C2N1CC3=CC=C(C=C3)C4=CC=CC=C4C(=O)O)C5=NC6=CC=CC=C6N5C)C';
|
|
8
9
|
// With bond preservation, double bonds are preserved in output
|
|
@@ -14,39 +15,39 @@ const BENZIMIDAZOLE_SMILES = 'c1nc2ccccc2n1';
|
|
|
14
15
|
// from the original due to fundamental AST model limitations:
|
|
15
16
|
// 1. Ring closures that span across branch depths can't be represented
|
|
16
17
|
// 2. The main fused ring (1+2) has atoms at different depths in the original SMILES
|
|
17
|
-
const TELMISARTAN_CODE_GENERATED = `const v1 = Linear(['C', 'C', 'C']);
|
|
18
|
-
const v2 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null] });
|
|
19
|
-
const v3 = v2.substitute(2, 'N');
|
|
20
|
-
const v4 = v3.substitute(5, 'N');
|
|
21
|
-
const v5 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null] });
|
|
22
|
-
const v6 = Linear(['C', 'C'], ['=']);
|
|
23
|
-
const v7 = Linear(['C', 'C', 'N', 'C'], ['=']);
|
|
24
|
-
const v8 = Ring({ atoms: 'C', size: 6, ringNumber: 3, bonds: ['=', null, '=', null, '=', null] });
|
|
25
|
-
const v9 = Ring({ atoms: 'C', size: 6, ringNumber: 4, bonds: ['=', null, '=', null, '=', null] });
|
|
26
|
-
const v10 = Linear(['C', 'O']);
|
|
27
|
-
const v11 = Linear(['O'], ['=']);
|
|
28
|
-
const v12 = v10.attach(v11, 1);
|
|
29
|
-
const v13 = v8.attach(v9, 4);
|
|
30
|
-
const v14 = v13.attach(v12, 6);
|
|
31
|
-
const v15 = Molecule([v7, v14]);
|
|
32
|
-
const v16 = v6.attach(v15, 2);
|
|
33
|
-
const v17 = Ring({ atoms: 'C', size: 5, ringNumber: 5, bonds: ['=', null, '=', null, null] });
|
|
34
|
-
const v18 = v17.substitute(2, 'N');
|
|
35
|
-
const v19 = v18.substitute(5, 'N');
|
|
36
|
-
const v20 = Ring({ atoms: 'C', size: 6, ringNumber: 6, bonds: ['=', null, '=', null, '=', null] });
|
|
37
|
-
const v21 = v19.fuse(v20, 2);
|
|
38
|
-
const v22 = Linear(['C']);
|
|
39
|
-
const v23 = Molecule([v16, v21, v22]);
|
|
40
|
-
const v24 = v5.attach(v23, 2);
|
|
41
|
-
const v25 = v4.fuse(v24, 2);
|
|
42
|
-
const v26 = Linear(['C']);
|
|
43
|
-
const v27 = Molecule([v1, v25, v26]);`;
|
|
44
|
-
|
|
45
|
-
const BENZIMIDAZOLE_CODE = `const v1 = Ring({ atoms: 'c', size: 5 });
|
|
46
|
-
const v2 = v1.substitute(2, 'n');
|
|
47
|
-
const v3 = v2.substitute(5, 'n');
|
|
48
|
-
const v4 = Ring({ atoms: 'c', size: 6, ringNumber: 2, offset: 2 });
|
|
49
|
-
const v5 = v3.fuse(v4, 2);`;
|
|
18
|
+
const TELMISARTAN_CODE_GENERATED = `export const v1 = Linear(['C', 'C', 'C']);
|
|
19
|
+
export const v2 = Ring({ atoms: 'C', size: 5, bonds: ['=', null, '=', null, null] });
|
|
20
|
+
export const v3 = v2.substitute(2, 'N');
|
|
21
|
+
export const v4 = v3.substitute(5, 'N');
|
|
22
|
+
export const v5 = Ring({ atoms: 'C', size: 6, ringNumber: 2, offset: 2, bonds: ['=', null, '=', null, '=', null] });
|
|
23
|
+
export const v6 = Linear(['C', 'C'], ['=']);
|
|
24
|
+
export const v7 = Linear(['C', 'C', 'N', 'C'], ['=']);
|
|
25
|
+
export const v8 = Ring({ atoms: 'C', size: 6, ringNumber: 3, bonds: ['=', null, '=', null, '=', null] });
|
|
26
|
+
export const v9 = Ring({ atoms: 'C', size: 6, ringNumber: 4, bonds: ['=', null, '=', null, '=', null] });
|
|
27
|
+
export const v10 = Linear(['C', 'O']);
|
|
28
|
+
export const v11 = Linear(['O'], ['=']);
|
|
29
|
+
export const v12 = v10.attach(v11, 1);
|
|
30
|
+
export const v13 = v8.attach(v9, 4);
|
|
31
|
+
export const v14 = v13.attach(v12, 6);
|
|
32
|
+
export const v15 = Molecule([v7, v14]);
|
|
33
|
+
export const v16 = v6.attach(v15, 2);
|
|
34
|
+
export const v17 = Ring({ atoms: 'C', size: 5, ringNumber: 5, bonds: ['=', null, '=', null, null] });
|
|
35
|
+
export const v18 = v17.substitute(2, 'N');
|
|
36
|
+
export const v19 = v18.substitute(5, 'N');
|
|
37
|
+
export const v20 = Ring({ atoms: 'C', size: 6, ringNumber: 6, bonds: ['=', null, '=', null, '=', null] });
|
|
38
|
+
export const v21 = v19.fuse(v20, 2);
|
|
39
|
+
export const v22 = Linear(['C']);
|
|
40
|
+
export const v23 = Molecule([v16, v21, v22]);
|
|
41
|
+
export const v24 = v5.attach(v23, 2);
|
|
42
|
+
export const v25 = v4.fuse(v24, 2);
|
|
43
|
+
export const v26 = Linear(['C']);
|
|
44
|
+
export const v27 = Molecule([v1, v25, v26]);`;
|
|
45
|
+
|
|
46
|
+
const BENZIMIDAZOLE_CODE = `export const v1 = Ring({ atoms: 'c', size: 5 });
|
|
47
|
+
export const v2 = v1.substitute(2, 'n');
|
|
48
|
+
export const v3 = v2.substitute(5, 'n');
|
|
49
|
+
export const v4 = Ring({ atoms: 'c', size: 6, ringNumber: 2, offset: 2 });
|
|
50
|
+
export const v5 = v3.fuse(v4, 2);`;
|
|
50
51
|
|
|
51
52
|
describe('Telmisartan Integration Test', () => {
|
|
52
53
|
test('parses telmisartan', () => {
|
|
@@ -107,11 +108,12 @@ describe('Telmisartan Integration Test', () => {
|
|
|
107
108
|
test('generated code is valid JavaScript', () => {
|
|
108
109
|
const ast = parse(TELMISARTAN_SMILES);
|
|
109
110
|
const code = ast.toCode('v');
|
|
111
|
+
const executableCode = stripExports(code);
|
|
110
112
|
|
|
111
113
|
let factory;
|
|
112
114
|
expect(() => {
|
|
113
115
|
// eslint-disable-next-line no-new-func
|
|
114
|
-
factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule',
|
|
116
|
+
factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
|
|
115
117
|
}).not.toThrow();
|
|
116
118
|
expect(typeof factory).toBe('function');
|
|
117
119
|
});
|
|
@@ -119,13 +121,14 @@ describe('Telmisartan Integration Test', () => {
|
|
|
119
121
|
test('generated code produces valid AST when executed', () => {
|
|
120
122
|
const ast = parse(TELMISARTAN_SMILES);
|
|
121
123
|
const code = ast.toCode('v');
|
|
124
|
+
const executableCode = stripExports(code);
|
|
122
125
|
|
|
123
126
|
// Find the last variable name
|
|
124
|
-
const varMatch = code.match(/const (v\d+) = /g);
|
|
125
|
-
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/const (v\d+)/)[1] : 'v1';
|
|
127
|
+
const varMatch = code.match(/export const (v\d+) = /g);
|
|
128
|
+
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1] : 'v1';
|
|
126
129
|
|
|
127
130
|
// eslint-disable-next-line no-new-func
|
|
128
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
131
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${lastVar};`);
|
|
129
132
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
130
133
|
|
|
131
134
|
// Note: SMILES won't match exactly due to inline ring closure limitation
|
|
@@ -141,13 +144,14 @@ describe('Telmisartan Integration Test', () => {
|
|
|
141
144
|
test('codegen round-trip: generated code produces valid SMILES', () => {
|
|
142
145
|
const ast = parse(TELMISARTAN_SMILES);
|
|
143
146
|
const code = ast.toCode('v');
|
|
147
|
+
const executableCode = stripExports(code);
|
|
144
148
|
|
|
145
149
|
// Find the last variable name
|
|
146
|
-
const varMatch = code.match(/const (v\d+) = /g);
|
|
147
|
-
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/const (v\d+)/)[1] : 'v1';
|
|
150
|
+
const varMatch = code.match(/export const (v\d+) = /g);
|
|
151
|
+
const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1] : 'v1';
|
|
148
152
|
|
|
149
153
|
// eslint-disable-next-line no-new-func
|
|
150
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
154
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${lastVar};`);
|
|
151
155
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
152
156
|
|
|
153
157
|
// The SMILES won't be identical but should be valid
|
|
@@ -158,11 +162,12 @@ describe('Telmisartan Integration Test', () => {
|
|
|
158
162
|
test('simple benzimidazole codegen round-trip', () => {
|
|
159
163
|
const ast = parse(BENZIMIDAZOLE_SMILES);
|
|
160
164
|
const code = ast.toCode('v');
|
|
165
|
+
const executableCode = stripExports(code);
|
|
161
166
|
|
|
162
167
|
expect(code).toBe(BENZIMIDAZOLE_CODE);
|
|
163
168
|
|
|
164
169
|
// eslint-disable-next-line no-new-func
|
|
165
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
170
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn v5;`);
|
|
166
171
|
const reconstructed = factory(Ring, Linear, FusedRing, Molecule);
|
|
167
172
|
|
|
168
173
|
expect(ast.smiles).toBe(BENZIMIDAZOLE_SMILES);
|
|
@@ -3,6 +3,14 @@ import {
|
|
|
3
3
|
Ring, Linear, FusedRing, Molecule,
|
|
4
4
|
} from '../src/constructors.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Strip 'export ' from code for execution in new Function()
|
|
8
|
+
* (new Function doesn't support ES module syntax)
|
|
9
|
+
*/
|
|
10
|
+
export function stripExports(code) {
|
|
11
|
+
return code.replace(/^export /gm, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Perform a codegen round-trip: SMILES -> AST -> CODE -> AST
|
|
8
16
|
* Returns the reconstructed AST node
|
|
@@ -14,15 +22,18 @@ export function codegenRoundTrip(smiles) {
|
|
|
14
22
|
const code = ast.toCode('v');
|
|
15
23
|
|
|
16
24
|
// Find the last variable name in the generated code
|
|
17
|
-
const varMatches = code.match(/const (v\d+)/g);
|
|
25
|
+
const varMatches = code.match(/export const (v\d+)/g);
|
|
18
26
|
if (!varMatches) {
|
|
19
27
|
throw new Error('No variables found in generated code');
|
|
20
28
|
}
|
|
21
|
-
const lastVar = varMatches[varMatches.length - 1].replace('const ', '');
|
|
29
|
+
const lastVar = varMatches[varMatches.length - 1].replace('export const ', '');
|
|
30
|
+
|
|
31
|
+
// Strip 'export ' for evaluation in new Function (not supported in non-module context)
|
|
32
|
+
const executableCode = code.replace(/^export /gm, '');
|
|
22
33
|
|
|
23
34
|
// Execute the code to reconstruct the AST
|
|
24
35
|
// eslint-disable-next-line no-new-func
|
|
25
|
-
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${
|
|
36
|
+
const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${lastVar};`);
|
|
26
37
|
return factory(Ring, Linear, FusedRing, Molecule);
|
|
27
38
|
}
|
|
28
39
|
|