smiles-js 2.0.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/API.md +162 -0
  2. package/README.md +39 -0
  3. package/docs/MIRROR_PLAN.md +204 -0
  4. package/docs/smiles.peggy +215 -0
  5. package/package.json +1 -1
  6. package/scripts/coverage-summary.js +1 -1
  7. package/src/codegen/branch-crossing-ring.js +27 -6
  8. package/src/codegen/interleaved-fused-ring.js +24 -0
  9. package/src/decompiler.js +236 -51
  10. package/src/decompiler.test.js +232 -60
  11. package/src/fragment.test.js +7 -2
  12. package/src/manipulation.js +409 -4
  13. package/src/manipulation.test.js +359 -1
  14. package/src/method-attachers.js +37 -8
  15. package/src/node-creators.js +7 -0
  16. package/src/parser/ast-builder.js +23 -8
  17. package/src/parser/ring-group-builder.js +14 -2
  18. package/src/parser/ring-utils.js +28 -0
  19. package/test-integration/__snapshots__/acetaminophen.test.js.snap +20 -0
  20. package/test-integration/__snapshots__/adjuvant-analgesics.test.js.snap +63 -1
  21. package/test-integration/__snapshots__/cholesterol-drugs.test.js.snap +437 -0
  22. package/test-integration/__snapshots__/dexamethasone.test.js.snap +31 -0
  23. package/test-integration/__snapshots__/endocannabinoids.test.js.snap +79 -2
  24. package/test-integration/__snapshots__/endogenous-opioids.test.js.snap +1116 -0
  25. package/test-integration/__snapshots__/hypertension-medication.test.js.snap +70 -1
  26. package/test-integration/__snapshots__/local-anesthetics.test.js.snap +97 -0
  27. package/test-integration/__snapshots__/nsaids-otc.test.js.snap +61 -1
  28. package/test-integration/__snapshots__/nsaids-prescription.test.js.snap +115 -2
  29. package/test-integration/__snapshots__/opioids.test.js.snap +113 -4
  30. package/test-integration/__snapshots__/steroids.test.js.snap +381 -2
  31. package/test-integration/acetaminophen.test.js +15 -3
  32. package/test-integration/adjuvant-analgesics.test.js +43 -7
  33. package/test-integration/cholesterol-drugs.test.js +127 -20
  34. package/test-integration/cholesterol.test.js +112 -0
  35. package/test-integration/dexamethasone.test.js +8 -2
  36. package/test-integration/endocannabinoids.test.js +48 -12
  37. package/test-integration/endogenous-opioids.smiles.js +32 -0
  38. package/test-integration/endogenous-opioids.test.js +192 -0
  39. package/test-integration/hypertension-medication.test.js +32 -8
  40. package/test-integration/local-anesthetics.smiles.js +33 -0
  41. package/test-integration/local-anesthetics.test.js +64 -16
  42. package/test-integration/mirror.test.js +151 -0
  43. package/test-integration/nsaids-otc.test.js +40 -10
  44. package/test-integration/nsaids-prescription.test.js +72 -18
  45. package/test-integration/opioids.test.js +56 -14
  46. package/test-integration/polymer.test.js +148 -0
  47. package/test-integration/steroids.test.js +112 -28
  48. package/test-integration/utils.js +4 -2
  49. package/todo +2 -3
@@ -23,6 +23,12 @@ describe('Gabapentin Integration Test', () => {
23
23
  expect(code).toMatchSnapshot();
24
24
  });
25
25
 
26
+ test('generates valid verbose code via toCode()', () => {
27
+ const ast = parse(GABAPENTIN_SMILES);
28
+ const code = ast.toCode('v', { verbose: true });
29
+ expect(code).toMatchSnapshot();
30
+ });
31
+
26
32
  test('generated code is valid JavaScript', () => {
27
33
  const ast = parse(GABAPENTIN_SMILES);
28
34
  const code = ast.toCode('v');
@@ -30,14 +36,14 @@ describe('Gabapentin Integration Test', () => {
30
36
 
31
37
  let factory;
32
38
  expect(() => {
33
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
39
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
34
40
  }).not.toThrow();
35
41
  expect(typeof factory).toBe('function');
36
42
  });
37
43
 
38
44
  test('codegen round-trip: generated code produces valid SMILES', () => {
39
45
  const ast = parse(GABAPENTIN_SMILES);
40
- const code = ast.toCode('v');
46
+ const code = ast.toCode('v', { verbose: true });
41
47
  const executableCode = stripExports(code);
42
48
 
43
49
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -64,9 +70,15 @@ describe('Pregabalin Integration Test', () => {
64
70
  expect(code).toMatchSnapshot();
65
71
  });
66
72
 
73
+ test('generates valid verbose code via toCode()', () => {
74
+ const ast = parse(PREGABALIN_SMILES);
75
+ const code = ast.toCode('v', { verbose: true });
76
+ expect(code).toMatchSnapshot();
77
+ });
78
+
67
79
  test('codegen round-trip: generated code produces valid SMILES', () => {
68
80
  const ast = parse(PREGABALIN_SMILES);
69
- const code = ast.toCode('v');
81
+ const code = ast.toCode('v', { verbose: true });
70
82
  const executableCode = stripExports(code);
71
83
 
72
84
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -92,9 +104,15 @@ describe('Amitriptyline Integration Test', () => {
92
104
  expect(code).toMatchSnapshot();
93
105
  });
94
106
 
107
+ test('generates valid verbose code via toCode()', () => {
108
+ const ast = parse(AMITRIPTYLINE_SMILES);
109
+ const code = ast.toCode('v', { verbose: true });
110
+ expect(code).toMatchSnapshot();
111
+ });
112
+
95
113
  test('codegen round-trip: generated code produces valid SMILES', () => {
96
114
  const ast = parse(AMITRIPTYLINE_SMILES);
97
- const code = ast.toCode('v');
115
+ const code = ast.toCode('v', { verbose: true });
98
116
  const executableCode = stripExports(code);
99
117
 
100
118
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -121,9 +139,15 @@ describe('Duloxetine Integration Test', () => {
121
139
  expect(code).toMatchSnapshot();
122
140
  });
123
141
 
142
+ test('generates valid verbose code via toCode()', () => {
143
+ const ast = parse(DULOXETINE_SMILES);
144
+ const code = ast.toCode('v', { verbose: true });
145
+ expect(code).toMatchSnapshot();
146
+ });
147
+
124
148
  test('codegen round-trip: generated code produces valid SMILES', () => {
125
149
  const ast = parse(DULOXETINE_SMILES);
126
- const code = ast.toCode('v');
150
+ const code = ast.toCode('v', { verbose: true });
127
151
  const executableCode = stripExports(code);
128
152
 
129
153
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -150,9 +174,15 @@ describe('Carbamazepine Integration Test', () => {
150
174
  expect(code).toMatchSnapshot();
151
175
  });
152
176
 
177
+ test('generates valid verbose code via toCode()', () => {
178
+ const ast = parse(CARBAMAZEPINE_SMILES);
179
+ const code = ast.toCode('v', { verbose: true });
180
+ expect(code).toMatchSnapshot();
181
+ });
182
+
153
183
  test('codegen round-trip: generated code produces valid SMILES', () => {
154
184
  const ast = parse(CARBAMAZEPINE_SMILES);
155
- const code = ast.toCode('v');
185
+ const code = ast.toCode('v', { verbose: true });
156
186
  const executableCode = stripExports(code);
157
187
 
158
188
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -179,9 +209,15 @@ describe('Valproic Acid Integration Test', () => {
179
209
  expect(code).toMatchSnapshot();
180
210
  });
181
211
 
212
+ test('generates valid verbose code via toCode()', () => {
213
+ const ast = parse(VALPROIC_ACID_SMILES);
214
+ const code = ast.toCode('v', { verbose: true });
215
+ expect(code).toMatchSnapshot();
216
+ });
217
+
182
218
  test('codegen round-trip: generated code produces valid SMILES', () => {
183
219
  const ast = parse(VALPROIC_ACID_SMILES);
184
- const code = ast.toCode('v');
220
+ const code = ast.toCode('v', { verbose: true });
185
221
  const executableCode = stripExports(code);
186
222
 
187
223
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -12,6 +12,7 @@ const EZETIMIBE_SMILES = 'C[C@H]1[C@@H](O)[C@H](O[C@H]1c2ccc(F)cc2)c3ccc(O)cc3C(
12
12
  const FENOFIBRATE_SMILES = 'CC(C)OC(=O)C(C)(C)Oc1ccc(cc1)C(=O)c2ccc(Cl)cc2';
13
13
  const GEMFIBROZIL_SMILES = 'CC1=CC(C)=CC=C1CCCC(C)(C)C(=O)O';
14
14
  const PITAVASTATIN_SMILES = 'CC(C)c1nc(nc(c1/C=C/[C@@H](O)C[C@@H](O)CC(=O)O)c2ccc(F)cc2)NC(=O)C3CC3';
15
+ const CHOLESTEROL_SMILES = 'C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C';
15
16
 
16
17
  describe('Atorvastatin Integration Test', () => {
17
18
  test('parses atorvastatin', () => {
@@ -27,6 +28,12 @@ describe('Atorvastatin Integration Test', () => {
27
28
  expect(code).toMatchSnapshot();
28
29
  });
29
30
 
31
+ test('generates valid verbose code via toCode()', () => {
32
+ const ast = parse(ATORVASTATIN_SMILES);
33
+ const code = ast.toCode('v', { verbose: true });
34
+ expect(code).toMatchSnapshot();
35
+ });
36
+
30
37
  test('generated code is valid JavaScript', () => {
31
38
  const ast = parse(ATORVASTATIN_SMILES);
32
39
  const code = ast.toCode('v');
@@ -34,14 +41,14 @@ describe('Atorvastatin Integration Test', () => {
34
41
 
35
42
  let factory;
36
43
  expect(() => {
37
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
44
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
38
45
  }).not.toThrow();
39
46
  expect(typeof factory).toBe('function');
40
47
  });
41
48
 
42
49
  test('codegen round-trip: generated code produces valid SMILES', () => {
43
50
  const ast = parse(ATORVASTATIN_SMILES);
44
- const code = ast.toCode('v');
51
+ const code = ast.toCode('v', { verbose: true });
45
52
  const executableCode = stripExports(code);
46
53
 
47
54
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -67,6 +74,12 @@ describe('Simvastatin Integration Test', () => {
67
74
  expect(code).toMatchSnapshot();
68
75
  });
69
76
 
77
+ test('generates valid verbose code via toCode()', () => {
78
+ const ast = parse(SIMVASTATIN_SMILES);
79
+ const code = ast.toCode('v', { verbose: true });
80
+ expect(code).toMatchSnapshot();
81
+ });
82
+
70
83
  test('generated code is valid JavaScript', () => {
71
84
  const ast = parse(SIMVASTATIN_SMILES);
72
85
  const code = ast.toCode('v');
@@ -74,14 +87,14 @@ describe('Simvastatin Integration Test', () => {
74
87
 
75
88
  let factory;
76
89
  expect(() => {
77
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
90
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
78
91
  }).not.toThrow();
79
92
  expect(typeof factory).toBe('function');
80
93
  });
81
94
 
82
95
  test('codegen round-trip: generated code produces valid SMILES', () => {
83
96
  const ast = parse(SIMVASTATIN_SMILES);
84
- const code = ast.toCode('v');
97
+ const code = ast.toCode('v', { verbose: true });
85
98
  const executableCode = stripExports(code);
86
99
 
87
100
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -107,6 +120,12 @@ describe('Rosuvastatin Integration Test', () => {
107
120
  expect(code).toMatchSnapshot();
108
121
  });
109
122
 
123
+ test('generates valid verbose code via toCode()', () => {
124
+ const ast = parse(ROSUVASTATIN_SMILES);
125
+ const code = ast.toCode('v', { verbose: true });
126
+ expect(code).toMatchSnapshot();
127
+ });
128
+
110
129
  test('generated code is valid JavaScript', () => {
111
130
  const ast = parse(ROSUVASTATIN_SMILES);
112
131
  const code = ast.toCode('v');
@@ -114,14 +133,14 @@ describe('Rosuvastatin Integration Test', () => {
114
133
 
115
134
  let factory;
116
135
  expect(() => {
117
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
136
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
118
137
  }).not.toThrow();
119
138
  expect(typeof factory).toBe('function');
120
139
  });
121
140
 
122
141
  test('codegen round-trip: generated code produces valid SMILES', () => {
123
142
  const ast = parse(ROSUVASTATIN_SMILES);
124
- const code = ast.toCode('v');
143
+ const code = ast.toCode('v', { verbose: true });
125
144
  const executableCode = stripExports(code);
126
145
 
127
146
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -147,6 +166,12 @@ describe('Pravastatin Integration Test', () => {
147
166
  expect(code).toMatchSnapshot();
148
167
  });
149
168
 
169
+ test('generates valid verbose code via toCode()', () => {
170
+ const ast = parse(PRAVASTATIN_SMILES);
171
+ const code = ast.toCode('v', { verbose: true });
172
+ expect(code).toMatchSnapshot();
173
+ });
174
+
150
175
  test('generated code is valid JavaScript', () => {
151
176
  const ast = parse(PRAVASTATIN_SMILES);
152
177
  const code = ast.toCode('v');
@@ -154,14 +179,14 @@ describe('Pravastatin Integration Test', () => {
154
179
 
155
180
  let factory;
156
181
  expect(() => {
157
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
182
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
158
183
  }).not.toThrow();
159
184
  expect(typeof factory).toBe('function');
160
185
  });
161
186
 
162
187
  test('codegen round-trip: generated code produces valid SMILES', () => {
163
188
  const ast = parse(PRAVASTATIN_SMILES);
164
- const code = ast.toCode('v');
189
+ const code = ast.toCode('v', { verbose: true });
165
190
  const executableCode = stripExports(code);
166
191
 
167
192
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -187,6 +212,12 @@ describe('Lovastatin Integration Test', () => {
187
212
  expect(code).toMatchSnapshot();
188
213
  });
189
214
 
215
+ test('generates valid verbose code via toCode()', () => {
216
+ const ast = parse(LOVASTATIN_SMILES);
217
+ const code = ast.toCode('v', { verbose: true });
218
+ expect(code).toMatchSnapshot();
219
+ });
220
+
190
221
  test('generated code is valid JavaScript', () => {
191
222
  const ast = parse(LOVASTATIN_SMILES);
192
223
  const code = ast.toCode('v');
@@ -194,14 +225,14 @@ describe('Lovastatin Integration Test', () => {
194
225
 
195
226
  let factory;
196
227
  expect(() => {
197
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
228
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
198
229
  }).not.toThrow();
199
230
  expect(typeof factory).toBe('function');
200
231
  });
201
232
 
202
233
  test('codegen round-trip: generated code produces valid SMILES', () => {
203
234
  const ast = parse(LOVASTATIN_SMILES);
204
- const code = ast.toCode('v');
235
+ const code = ast.toCode('v', { verbose: true });
205
236
  const executableCode = stripExports(code);
206
237
 
207
238
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -227,6 +258,12 @@ describe('Fluvastatin Integration Test', () => {
227
258
  expect(code).toMatchSnapshot();
228
259
  });
229
260
 
261
+ test('generates valid verbose code via toCode()', () => {
262
+ const ast = parse(FLUVASTATIN_SMILES);
263
+ const code = ast.toCode('v', { verbose: true });
264
+ expect(code).toMatchSnapshot();
265
+ });
266
+
230
267
  test('generated code is valid JavaScript', () => {
231
268
  const ast = parse(FLUVASTATIN_SMILES);
232
269
  const code = ast.toCode('v');
@@ -234,14 +271,14 @@ describe('Fluvastatin Integration Test', () => {
234
271
 
235
272
  let factory;
236
273
  expect(() => {
237
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
274
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
238
275
  }).not.toThrow();
239
276
  expect(typeof factory).toBe('function');
240
277
  });
241
278
 
242
279
  test('codegen round-trip: generated code produces valid SMILES', () => {
243
280
  const ast = parse(FLUVASTATIN_SMILES);
244
- const code = ast.toCode('v');
281
+ const code = ast.toCode('v', { verbose: true });
245
282
  const executableCode = stripExports(code);
246
283
 
247
284
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -267,6 +304,12 @@ describe('Ezetimibe Integration Test', () => {
267
304
  expect(code).toMatchSnapshot();
268
305
  });
269
306
 
307
+ test('generates valid verbose code via toCode()', () => {
308
+ const ast = parse(EZETIMIBE_SMILES);
309
+ const code = ast.toCode('v', { verbose: true });
310
+ expect(code).toMatchSnapshot();
311
+ });
312
+
270
313
  test('generated code is valid JavaScript', () => {
271
314
  const ast = parse(EZETIMIBE_SMILES);
272
315
  const code = ast.toCode('v');
@@ -274,14 +317,14 @@ describe('Ezetimibe Integration Test', () => {
274
317
 
275
318
  let factory;
276
319
  expect(() => {
277
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
320
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
278
321
  }).not.toThrow();
279
322
  expect(typeof factory).toBe('function');
280
323
  });
281
324
 
282
325
  test('codegen round-trip: generated code produces valid SMILES', () => {
283
326
  const ast = parse(EZETIMIBE_SMILES);
284
- const code = ast.toCode('v');
327
+ const code = ast.toCode('v', { verbose: true });
285
328
  const executableCode = stripExports(code);
286
329
 
287
330
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -307,6 +350,12 @@ describe('Fenofibrate Integration Test', () => {
307
350
  expect(code).toMatchSnapshot();
308
351
  });
309
352
 
353
+ test('generates valid verbose code via toCode()', () => {
354
+ const ast = parse(FENOFIBRATE_SMILES);
355
+ const code = ast.toCode('v', { verbose: true });
356
+ expect(code).toMatchSnapshot();
357
+ });
358
+
310
359
  test('generated code is valid JavaScript', () => {
311
360
  const ast = parse(FENOFIBRATE_SMILES);
312
361
  const code = ast.toCode('v');
@@ -314,14 +363,14 @@ describe('Fenofibrate Integration Test', () => {
314
363
 
315
364
  let factory;
316
365
  expect(() => {
317
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
366
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
318
367
  }).not.toThrow();
319
368
  expect(typeof factory).toBe('function');
320
369
  });
321
370
 
322
371
  test('codegen round-trip: generated code produces valid SMILES', () => {
323
372
  const ast = parse(FENOFIBRATE_SMILES);
324
- const code = ast.toCode('v');
373
+ const code = ast.toCode('v', { verbose: true });
325
374
  const executableCode = stripExports(code);
326
375
 
327
376
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -347,6 +396,12 @@ describe('Gemfibrozil Integration Test', () => {
347
396
  expect(code).toMatchSnapshot();
348
397
  });
349
398
 
399
+ test('generates valid verbose code via toCode()', () => {
400
+ const ast = parse(GEMFIBROZIL_SMILES);
401
+ const code = ast.toCode('v', { verbose: true });
402
+ expect(code).toMatchSnapshot();
403
+ });
404
+
350
405
  test('generated code is valid JavaScript', () => {
351
406
  const ast = parse(GEMFIBROZIL_SMILES);
352
407
  const code = ast.toCode('v');
@@ -354,14 +409,14 @@ describe('Gemfibrozil Integration Test', () => {
354
409
 
355
410
  let factory;
356
411
  expect(() => {
357
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
412
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
358
413
  }).not.toThrow();
359
414
  expect(typeof factory).toBe('function');
360
415
  });
361
416
 
362
417
  test('codegen round-trip: generated code produces valid SMILES', () => {
363
418
  const ast = parse(GEMFIBROZIL_SMILES);
364
- const code = ast.toCode('v');
419
+ const code = ast.toCode('v', { verbose: true });
365
420
  const executableCode = stripExports(code);
366
421
 
367
422
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -387,6 +442,12 @@ describe('Pitavastatin Integration Test', () => {
387
442
  expect(code).toMatchSnapshot();
388
443
  });
389
444
 
445
+ test('generates valid verbose code via toCode()', () => {
446
+ const ast = parse(PITAVASTATIN_SMILES);
447
+ const code = ast.toCode('v', { verbose: true });
448
+ expect(code).toMatchSnapshot();
449
+ });
450
+
390
451
  test('generated code is valid JavaScript', () => {
391
452
  const ast = parse(PITAVASTATIN_SMILES);
392
453
  const code = ast.toCode('v');
@@ -394,14 +455,14 @@ describe('Pitavastatin Integration Test', () => {
394
455
 
395
456
  let factory;
396
457
  expect(() => {
397
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
458
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
398
459
  }).not.toThrow();
399
460
  expect(typeof factory).toBe('function');
400
461
  });
401
462
 
402
463
  test('codegen round-trip: generated code produces valid SMILES', () => {
403
464
  const ast = parse(PITAVASTATIN_SMILES);
404
- const code = ast.toCode('v');
465
+ const code = ast.toCode('v', { verbose: true });
405
466
  const executableCode = stripExports(code);
406
467
 
407
468
  const varMatch = code.match(/export const (v\d+) = /g);
@@ -412,3 +473,49 @@ describe('Pitavastatin Integration Test', () => {
412
473
  expect(reconstructed.smiles).toBe(PITAVASTATIN_SMILES);
413
474
  });
414
475
  });
476
+
477
+ describe('Cholesterol Integration Test', () => {
478
+ test('parses cholesterol', () => {
479
+ const ast = parse(CHOLESTEROL_SMILES);
480
+ const obj = ast.toObject();
481
+ expect(obj).toMatchSnapshot();
482
+ expect(ast.smiles).toBe(CHOLESTEROL_SMILES);
483
+ });
484
+
485
+ test('generates valid code via toCode()', () => {
486
+ const ast = parse(CHOLESTEROL_SMILES);
487
+ const code = ast.toCode('v');
488
+ expect(code).toMatchSnapshot();
489
+ });
490
+
491
+ test('generates valid verbose code via toCode()', () => {
492
+ const ast = parse(CHOLESTEROL_SMILES);
493
+ const code = ast.toCode('v', { verbose: true });
494
+ expect(code).toMatchSnapshot();
495
+ });
496
+
497
+ test('generated code is valid JavaScript', () => {
498
+ const ast = parse(CHOLESTEROL_SMILES);
499
+ const code = ast.toCode('v');
500
+ const executableCode = stripExports(code);
501
+
502
+ let factory;
503
+ expect(() => {
504
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
505
+ }).not.toThrow();
506
+ expect(typeof factory).toBe('function');
507
+ });
508
+
509
+ test('codegen round-trip: generated code produces valid SMILES', () => {
510
+ const ast = parse(CHOLESTEROL_SMILES);
511
+ const code = ast.toCode('v', { verbose: true });
512
+ const executableCode = stripExports(code);
513
+
514
+ const varMatch = code.match(/export const (v\d+) = /g);
515
+ const lastVar = varMatch ? varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1] : 'v1';
516
+
517
+ const reconstructed = executeCode(executableCode, lastVar);
518
+
519
+ expect(reconstructed.smiles).toBe(CHOLESTEROL_SMILES);
520
+ });
521
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { parse } from '../src/parser/index.js';
3
+ import { codegenRoundTrip } from './utils.js';
4
+
5
+ const CHOLESTEROL_SMILES = 'C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C';
6
+
7
+ describe('Cholesterol - Divide and Conquer', () => {
8
+ // Cholesterol SMILES:
9
+ // C[C@H](CCCC(C)C)[C@H]1CC[C@@H]2[C@@]1(CC[C@H]3[C@H]2CC=C4[C@@]3(CC[C@@H](C4)O)C)C
10
+ //
11
+ // The failure: ring 4 is being dropped
12
+ // Expected: ...CC=C4[C@@]3(CC[C@@H](C4)O)C)C
13
+ // Received: ...CC=C[C@@]3(CC[C@@H](C)O)C)C
14
+
15
+ // Level 1: Simple ring closures
16
+ test('simple ring', () => {
17
+ const s = 'C1CCCCC1';
18
+ expect(parse(s).smiles).toBe(s);
19
+ expect(codegenRoundTrip(s).smiles).toBe(s);
20
+ });
21
+
22
+ // Level 2: Two fused rings (decalin-like)
23
+ test('two fused rings', () => {
24
+ const s = 'C1CCC2CCCCC2C1';
25
+ expect(parse(s).smiles).toBe(s);
26
+ expect(codegenRoundTrip(s).smiles).toBe(s);
27
+ });
28
+
29
+ // Level 3: Ring closure inside a branch - the core pattern
30
+ test('ring closure in branch: C1CCC(C1)C', () => {
31
+ const s = 'C1CCC(C1)C';
32
+ expect(parse(s).smiles).toBe(s);
33
+ expect(codegenRoundTrip(s).smiles).toBe(s);
34
+ });
35
+
36
+ // Level 4: Two ring numbers, one closing in a branch
37
+ test('two rings, one closing in branch: C1CC2CCC(C2)C1', () => {
38
+ const s = 'C1CC2CCC(C2)C1';
39
+ expect(parse(s).smiles).toBe(s);
40
+ expect(codegenRoundTrip(s).smiles).toBe(s);
41
+ });
42
+
43
+ // Level 5: Ring opening and closing with different numbers in nested context
44
+ test('ring 4 opens and closes in branch: C4CCC(C4)', () => {
45
+ const s = 'C4CCC(C4)';
46
+ expect(parse(s).smiles).toBe(s);
47
+ expect(codegenRoundTrip(s).smiles).toBe(s);
48
+ });
49
+
50
+ // Level 6: The specific pattern from cholesterol - ring opens, then closes inside a parenthesized branch
51
+ test('ring open at atom, close inside branch argument: CC=C4C(CC(C4)O)', () => {
52
+ const s = 'CC=C4C(CC(C4)O)';
53
+ expect(parse(s).smiles).toBe(s);
54
+ expect(codegenRoundTrip(s).smiles).toBe(s);
55
+ });
56
+
57
+ // Level 7: Closer to cholesterol D-ring pattern
58
+ test('cholesterol D-ring pattern: CC=C4C(CCC(C4)O)C', () => {
59
+ const s = 'CC=C4C(CCC(C4)O)C';
60
+ expect(parse(s).smiles).toBe(s);
61
+ expect(codegenRoundTrip(s).smiles).toBe(s);
62
+ });
63
+
64
+ // Level 8: Add ring 3 fused with ring 4
65
+ test('fused rings 3+4: C3CC=C4C3(CCC(C4)O)C', () => {
66
+ const s = 'C3CC=C4C3(CCC(C4)O)C';
67
+ expect(parse(s).smiles).toBe(s);
68
+ expect(codegenRoundTrip(s).smiles).toBe(s);
69
+ });
70
+
71
+ // Level 9: The C/D ring system from cholesterol (without stereo)
72
+ test('C/D ring system: C2CC=C4C(CC2)(CCC(C4)O)C', () => {
73
+ const s = 'C2CC=C4C(CC2)(CCC(C4)O)C';
74
+ expect(parse(s).smiles).toBe(s);
75
+ expect(codegenRoundTrip(s).smiles).toBe(s);
76
+ });
77
+
78
+ // Level 10: Three fused rings (A/B/C ring without stereo)
79
+ test('three fused rings: C1CCC2C1CCC3CC=C4C3(CCC(C4)O)C', () => {
80
+ const s = 'C1CCC2C1CCC3C2CC=C4C3(CCC(C4)O)C';
81
+ expect(parse(s).smiles).toBe(s);
82
+ expect(codegenRoundTrip(s).smiles).toBe(s);
83
+ });
84
+
85
+ // Level 11: Isolate multiple branches on same atom with ring closures
86
+ test('two branches with ring closures: C1C(C1)(C2)CC2', () => {
87
+ const s = 'C1C(C1)(C2)CC2';
88
+ expect(parse(s).smiles).toBe(s);
89
+ expect(codegenRoundTrip(s).smiles).toBe(s);
90
+ });
91
+
92
+ // Level 12: Minimal failing pattern - ring closure in second branch
93
+ test('ring close in second branch: C4CC(C)(C4)', () => {
94
+ const s = 'C4CC(C)(C4)';
95
+ expect(parse(s).smiles).toBe(s);
96
+ expect(codegenRoundTrip(s).smiles).toBe(s);
97
+ });
98
+
99
+ // Level 13: Full cholesterol (no stereo) to isolate stereo vs ring issue
100
+ test('cholesterol without stereochemistry', () => {
101
+ const s = 'CC(CCCC(C)C)C1CCC2C1(CCC3C2CC=C4C3(CCC(C4)O)C)C';
102
+ expect(parse(s).smiles).toBe(s);
103
+ expect(codegenRoundTrip(s).smiles).toBe(s);
104
+ });
105
+
106
+ // Level 14: Full cholesterol with stereochemistry
107
+ test('full cholesterol', () => {
108
+ const s = CHOLESTEROL_SMILES;
109
+ expect(parse(s).smiles).toBe(s);
110
+ expect(codegenRoundTrip(s).smiles).toBe(s);
111
+ });
112
+ });
@@ -24,6 +24,12 @@ describe('Dexamethasone Integration Test', () => {
24
24
  expect(code).toMatchSnapshot();
25
25
  });
26
26
 
27
+ test('generates valid verbose code via toCode()', () => {
28
+ const ast = parse(DEXAMETHASONE_SMILES);
29
+ const code = ast.toCode('v', { verbose: true });
30
+ expect(code).toMatchSnapshot();
31
+ });
32
+
27
33
  test('generated code is valid JavaScript', () => {
28
34
  const ast = parse(DEXAMETHASONE_SMILES);
29
35
  const code = ast.toCode('v');
@@ -31,14 +37,14 @@ describe('Dexamethasone Integration Test', () => {
31
37
 
32
38
  let factory;
33
39
  expect(() => {
34
- factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', executableCode);
40
+ factory = createFunction('Ring', 'Linear', 'FusedRing', 'Molecule', 'RawFragment', 'Fragment', executableCode);
35
41
  }).not.toThrow();
36
42
  expect(typeof factory).toBe('function');
37
43
  });
38
44
 
39
45
  test('codegen round-trip: generated code produces valid SMILES', () => {
40
46
  const ast = parse(DEXAMETHASONE_SMILES);
41
- const code = ast.toCode('v');
47
+ const code = ast.toCode('v', { verbose: true });
42
48
  const executableCode = stripExports(code);
43
49
  const varMatch = code.match(/export const (v\d+) = /g);
44
50
  const lastVar = varMatch[varMatch.length - 1].match(/export const (v\d+)/)[1];