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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smiles-js",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "A JavaScript library for building molecules using composable fragments",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
- return code;
820
+
821
+ // Always use export const
822
+ return code.replace(/^(\s*)const /gm, '$1export const ');
817
823
  }
@@ -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
  });
@@ -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', code);
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', `${code}\nreturn ${lastVar};`);
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', `${code}\nreturn ${lastVar};`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', `${code}\nreturn v13;`);
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', `${code}\nreturn v7;`);
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', `${code}\nreturn v5;`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', `${code}\nreturn v10;`);
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', `${code}\nreturn v7;`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', code);
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', `${code}\nreturn ${data.lastVar};`);
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', `${code}\nreturn v7;`);
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', `${code}\nreturn v11;`);
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', code);
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', `${code}\nreturn ${lastVar};`);
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', `${code}\nreturn ${lastVar};`);
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', `${code}\nreturn v5;`);
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', `${code}\nreturn ${lastVar};`);
36
+ const factory = new Function('Ring', 'Linear', 'FusedRing', 'Molecule', `${executableCode}\nreturn ${lastVar};`);
26
37
  return factory(Ring, Linear, FusedRing, Molecule);
27
38
  }
28
39