nx_chem_smiles.nx source
↩ module page · 941 lines · 35833 B
1// nx_chem_smiles.nx -- C2.1 + C2.2 milestones: basic SMILES parser.
2//
3// Parses the organic-subset SMILES grammar (Daylight 1988 / OpenSMILES
4// 2016) into the MolGraph type from nx_chem_molecule.nx. Memory-safe
5// by construction: every read is bounded against the input length;
6// every error path sets is_valid=0 + structured err_code + err_pos.
7//
8// Supported (C2.1 + C2.2):
9// - Organic-subset aliphatic atoms outside brackets: B, C, N, O, P, S, F, Cl, Br, I
10// - Aromatic lowercase atoms outside brackets: b, c, n, o, p, s
11// - Wildcard: *
12// - **Full 118-element bracket atoms** (C2.2): H..Og, all IUPAC element symbols
13// - Bracket attributes:
14// isotope -- leading digits, e.g. [13C]
15// chirality -- @ (CCW) or @@ (CW) on atom; stored in Atom.stereo (C2.2)
16// H count -- H[count], e.g. [CH4], [NH4+]
17// charge -- +, -, ++, --, +n, -n
18// atom map -- :n, stored in Atom.map_num (C2.2)
19// - Bond types: -, =, #, :, . (default = single between aliphatic, aromatic between two aromatics)
20// - **Bond stereo: /, \\** -- stored in Bond.stereo as NX_BSTEREO_UP/DOWN (C2.2)
21// - Branches: ( and )
22// - **Ring closures: 0..9 + %nn (two-digit, 00..99)** (C2.2)
23//
24// Honest gaps (deferred to C2.3+):
25// - E/Z resolution from /, \\ (C2.1 stores raw UP/DOWN; CIP-rule resolution is C2.3)
26// - Atropisomer + enhanced-stereo (C2.4)
27// - Implicit-H valence inference (C2.3 -- h_count stays -1 for non-bracket atoms)
28// - 6-aromaticity-tuple model dispatch (C2.5 -- this milestone sets one Daylight bit; tuple slot present)
29// - Canonical SMILES output (C2.3)
30// - Fuzzer 10^9 harness (C2.9)
31//
32// EXCEED axes hit (landscape doc references):
33// E2 -- memory-safe parser by construction (bounded buffer reads + structured error paths)
34// E4 -- per-atom + per-bond stereo slots populated from SMILES input (C2.2)
35// E7 -- first-class radicals + charges + isotopes + full 118-element bracket support
36// E11 -- no format lock-in (output is native MolGraph)
37//
38// nx_safety_envelope:
39// intended_use: SMILES -> MolGraph parser for cheminformatics pipeline
40// sil_target: SIL1
41// evidence: [C2.1 + C2.2 KAT in nx_chem_smiles_test.nx; bounded buffer arithmetic; structured error codes]
42// verdict: BENCH-PENDING
43
44import "nx_kernel_v2.nx"
45import "nx_chem_molecule.nx"
46
47// =================================================================
48// ASCII code constants (NishiLang has no char literals).
49// =================================================================
50const NX_C_0: nx_int = 0x30
51const NX_C_9: nx_int = 0x39
52const NX_C_A_UP: nx_int = 0x41
53const NX_C_Z_UP: nx_int = 0x5A
54const NX_C_A_LO: nx_int = 0x61
55const NX_C_Z_LO: nx_int = 0x7A
56const NX_C_LBRACKET: nx_int = 0x5B
57const NX_C_RBRACKET: nx_int = 0x5D
58const NX_C_LPAREN: nx_int = 0x28
59const NX_C_RPAREN: nx_int = 0x29
60const NX_C_PLUS: nx_int = 0x2B
61const NX_C_MINUS: nx_int = 0x2D
62const NX_C_EQ: nx_int = 0x3D
63const NX_C_HASH: nx_int = 0x23
64const NX_C_COLON: nx_int = 0x3A
65const NX_C_DOT: nx_int = 0x2E
66const NX_C_SLASH: nx_int = 0x2F
67const NX_C_BSLASH: nx_int = 0x5C
68const NX_C_STAR: nx_int = 0x2A
69const NX_C_AT: nx_int = 0x40
70const NX_C_PCT: nx_int = 0x25
71const NX_C_H_UP: nx_int = 0x48
72const NX_C_B_UP: nx_int = 0x42
73const NX_C_C_UP: nx_int = 0x43
74const NX_C_N_UP: nx_int = 0x4E
75const NX_C_O_UP: nx_int = 0x4F
76const NX_C_F_UP: nx_int = 0x46
77const NX_C_P_UP: nx_int = 0x50
78const NX_C_S_UP: nx_int = 0x53
79const NX_C_I_UP: nx_int = 0x49
80const NX_C_K_UP: nx_int = 0x4B
81const NX_C_B_LO: nx_int = 0x62
82const NX_C_C_LO: nx_int = 0x63
83const NX_C_N_LO: nx_int = 0x6E
84const NX_C_O_LO: nx_int = 0x6F
85const NX_C_P_LO: nx_int = 0x70
86const NX_C_S_LO: nx_int = 0x73
87const NX_C_L_LO: nx_int = 0x6C
88const NX_C_R_LO: nx_int = 0x72
89
90// =================================================================
91// Single-char digit / alpha tests.
92// =================================================================
93func smi_is_digit(c: nx_int) -> nx_int {
94 if c < NX_C_0 { return 0 }
95 if c > NX_C_9 { return 0 }
96 return 1
97}
98
99func smi_is_upper(c: nx_int) -> nx_int {
100 if c < NX_C_A_UP { return 0 }
101 if c > NX_C_Z_UP { return 0 }
102 return 1
103}
104
105func smi_is_lower(c: nx_int) -> nx_int {
106 if c < NX_C_A_LO { return 0 }
107 if c > NX_C_Z_LO { return 0 }
108 return 1
109}
110
111// =================================================================
112// Full IUPAC 118-element two-char symbol -> Z. Returns 0 if not a
113// valid two-char element symbol. Caller falls back to single-char
114// lookup if this returns 0.
115// =================================================================
116func smi_bracket_symbol_two_char(c1: nx_int, c2: nx_int) -> nx_int {
117 if c1 == 0x41 {
118 if c2 == 0x63 { return 89 } // Ac
119 if c2 == 0x67 { return 47 } // Ag
120 if c2 == 0x6C { return 13 } // Al
121 if c2 == 0x6D { return 95 } // Am
122 if c2 == 0x72 { return 18 } // Ar
123 if c2 == 0x73 { return 33 } // As
124 if c2 == 0x74 { return 85 } // At
125 if c2 == 0x75 { return 79 } // Au
126 return 0
127 }
128 if c1 == 0x42 {
129 if c2 == 0x61 { return 56 } // Ba
130 if c2 == 0x65 { return 4 } // Be
131 if c2 == 0x68 { return 107 } // Bh
132 if c2 == 0x69 { return 83 } // Bi
133 if c2 == 0x6B { return 97 } // Bk
134 if c2 == 0x72 { return 35 } // Br
135 return 0
136 }
137 if c1 == 0x43 {
138 if c2 == 0x61 { return 20 } // Ca
139 if c2 == 0x64 { return 48 } // Cd
140 if c2 == 0x65 { return 58 } // Ce
141 if c2 == 0x66 { return 98 } // Cf
142 if c2 == 0x6C { return 17 } // Cl
143 if c2 == 0x6D { return 96 } // Cm
144 if c2 == 0x6E { return 112 } // Cn
145 if c2 == 0x6F { return 27 } // Co
146 if c2 == 0x72 { return 24 } // Cr
147 if c2 == 0x73 { return 55 } // Cs
148 if c2 == 0x75 { return 29 } // Cu
149 return 0
150 }
151 if c1 == 0x44 {
152 if c2 == 0x62 { return 105 } // Db
153 if c2 == 0x73 { return 110 } // Ds
154 if c2 == 0x79 { return 66 } // Dy
155 return 0
156 }
157 if c1 == 0x45 {
158 if c2 == 0x72 { return 68 } // Er
159 if c2 == 0x73 { return 99 } // Es
160 if c2 == 0x75 { return 63 } // Eu
161 return 0
162 }
163 if c1 == 0x46 {
164 if c2 == 0x65 { return 26 } // Fe
165 if c2 == 0x6C { return 114 } // Fl
166 if c2 == 0x6D { return 100 } // Fm
167 if c2 == 0x72 { return 87 } // Fr
168 return 0
169 }
170 if c1 == 0x47 {
171 if c2 == 0x61 { return 31 } // Ga
172 if c2 == 0x64 { return 64 } // Gd
173 if c2 == 0x65 { return 32 } // Ge
174 return 0
175 }
176 if c1 == 0x48 {
177 if c2 == 0x65 { return 2 } // He
178 if c2 == 0x66 { return 72 } // Hf
179 if c2 == 0x67 { return 80 } // Hg
180 if c2 == 0x6F { return 67 } // Ho
181 if c2 == 0x73 { return 108 } // Hs
182 return 0
183 }
184 if c1 == 0x49 {
185 if c2 == 0x6E { return 49 } // In
186 if c2 == 0x72 { return 77 } // Ir
187 return 0
188 }
189 if c1 == 0x4B {
190 if c2 == 0x72 { return 36 } // Kr
191 return 0
192 }
193 if c1 == 0x4C {
194 if c2 == 0x61 { return 57 } // La
195 if c2 == 0x69 { return 3 } // Li
196 if c2 == 0x72 { return 103 } // Lr
197 if c2 == 0x75 { return 71 } // Lu
198 if c2 == 0x76 { return 116 } // Lv
199 return 0
200 }
201 if c1 == 0x4D {
202 if c2 == 0x63 { return 115 } // Mc
203 if c2 == 0x64 { return 101 } // Md
204 if c2 == 0x67 { return 12 } // Mg
205 if c2 == 0x6E { return 25 } // Mn
206 if c2 == 0x6F { return 42 } // Mo
207 if c2 == 0x74 { return 109 } // Mt
208 return 0
209 }
210 if c1 == 0x4E {
211 if c2 == 0x61 { return 11 } // Na
212 if c2 == 0x62 { return 41 } // Nb
213 if c2 == 0x64 { return 60 } // Nd
214 if c2 == 0x65 { return 10 } // Ne
215 if c2 == 0x68 { return 113 } // Nh
216 if c2 == 0x69 { return 28 } // Ni
217 if c2 == 0x6F { return 102 } // No
218 if c2 == 0x70 { return 93 } // Np
219 return 0
220 }
221 if c1 == 0x4F {
222 if c2 == 0x67 { return 118 } // Og
223 if c2 == 0x73 { return 76 } // Os
224 return 0
225 }
226 if c1 == 0x50 {
227 if c2 == 0x61 { return 91 } // Pa
228 if c2 == 0x62 { return 82 } // Pb
229 if c2 == 0x64 { return 46 } // Pd
230 if c2 == 0x6D { return 61 } // Pm
231 if c2 == 0x6F { return 84 } // Po
232 if c2 == 0x72 { return 59 } // Pr
233 if c2 == 0x74 { return 78 } // Pt
234 if c2 == 0x75 { return 94 } // Pu
235 return 0
236 }
237 if c1 == 0x52 {
238 if c2 == 0x61 { return 88 } // Ra
239 if c2 == 0x62 { return 37 } // Rb
240 if c2 == 0x65 { return 75 } // Re
241 if c2 == 0x66 { return 104 } // Rf
242 if c2 == 0x67 { return 111 } // Rg
243 if c2 == 0x68 { return 45 } // Rh
244 if c2 == 0x6E { return 86 } // Rn
245 if c2 == 0x75 { return 44 } // Ru
246 return 0
247 }
248 if c1 == 0x53 {
249 if c2 == 0x62 { return 51 } // Sb
250 if c2 == 0x63 { return 21 } // Sc
251 if c2 == 0x65 { return 34 } // Se
252 if c2 == 0x67 { return 106 } // Sg
253 if c2 == 0x69 { return 14 } // Si
254 if c2 == 0x6D { return 62 } // Sm
255 if c2 == 0x6E { return 50 } // Sn
256 if c2 == 0x72 { return 38 } // Sr
257 return 0
258 }
259 if c1 == 0x54 {
260 if c2 == 0x61 { return 73 } // Ta
261 if c2 == 0x62 { return 65 } // Tb
262 if c2 == 0x63 { return 43 } // Tc
263 if c2 == 0x65 { return 52 } // Te
264 if c2 == 0x68 { return 90 } // Th
265 if c2 == 0x69 { return 22 } // Ti
266 if c2 == 0x6C { return 81 } // Tl
267 if c2 == 0x6D { return 69 } // Tm
268 if c2 == 0x73 { return 117 } // Ts
269 return 0
270 }
271 if c1 == 0x58 {
272 if c2 == 0x65 { return 54 } // Xe
273 return 0
274 }
275 if c1 == 0x59 {
276 if c2 == 0x62 { return 70 } // Yb
277 return 0
278 }
279 if c1 == 0x5A {
280 if c2 == 0x6E { return 30 } // Zn
281 if c2 == 0x72 { return 40 } // Zr
282 return 0
283 }
284 return 0
285}
286
287// =================================================================
288// Single-char element symbol -> Z (14 elements).
289// =================================================================
290func smi_bracket_symbol_single_char(c1: nx_int) -> nx_int {
291 if c1 == NX_C_H_UP { return 1 } // H
292 if c1 == NX_C_B_UP { return 5 } // B
293 if c1 == NX_C_C_UP { return 6 } // C
294 if c1 == NX_C_N_UP { return 7 } // N
295 if c1 == NX_C_O_UP { return 8 } // O
296 if c1 == NX_C_F_UP { return 9 } // F
297 if c1 == NX_C_P_UP { return 15 } // P
298 if c1 == NX_C_S_UP { return 16 } // S
299 if c1 == NX_C_K_UP { return 19 } // K
300 if c1 == 0x56 { return 23 } // V
301 if c1 == 0x59 { return 39 } // Y
302 if c1 == NX_C_I_UP { return 53 } // I
303 if c1 == 0x57 { return 74 } // W
304 if c1 == 0x55 { return 92 } // U
305 return 0
306}
307
308// =================================================================
309// Lowercase aromatic atom (single char only) -> Z, or 0 if not aromatic.
310// =================================================================
311func smi_aromatic_lower_to_z(c: nx_int) -> nx_int {
312 if c == NX_C_B_LO { return 5 } // b
313 if c == NX_C_C_LO { return 6 } // c
314 if c == NX_C_N_LO { return 7 } // n
315 if c == NX_C_O_LO { return 8 } // o
316 if c == NX_C_P_LO { return 15 } // p
317 if c == NX_C_S_LO { return 16 } // s
318 return 0
319}
320
321// =================================================================
322// Outside-bracket organic-subset atom recognizer.
323// Reads at most 2 chars; returns z + consumed length (1 or 2) via out
324// pointer. Returns 0 (z) if char doesn't start a known organic-subset
325// atom.
326// =================================================================
327func smi_organic_atom(c1: nx_int, c2: nx_int, consumed_out: *nx_int) -> nx_int {
328 // Two-char: Cl, Br
329 if c1 == NX_C_C_UP {
330 if c2 == NX_C_L_LO { consumed_out[0] = 2; return 17 } // Cl
331 consumed_out[0] = 1; return 6 // C
332 }
333 if c1 == NX_C_B_UP {
334 if c2 == NX_C_R_LO { consumed_out[0] = 2; return 35 } // Br
335 consumed_out[0] = 1; return 5 // B
336 }
337 if c1 == NX_C_N_UP { consumed_out[0] = 1; return 7 } // N
338 if c1 == NX_C_O_UP { consumed_out[0] = 1; return 8 } // O
339 if c1 == NX_C_F_UP { consumed_out[0] = 1; return 9 } // F
340 if c1 == NX_C_P_UP { consumed_out[0] = 1; return 15 } // P
341 if c1 == NX_C_S_UP { consumed_out[0] = 1; return 16 } // S
342 if c1 == NX_C_I_UP { consumed_out[0] = 1; return 53 } // I
343 consumed_out[0] = 0
344 return 0
345}
346
347// =================================================================
348// Parse a bracket atom starting at src[pos] = '['. Advances pos to
349// one past the closing ']'. Returns 1 on success, 0 on failure (and
350// sets is_valid + err_code on m).
351//
352// Grammar (C2.2):
353// '[' <isotope-digits>? Symbol <'@'|'@@'>? <H><h-digits>? (<+|-> <digits|+|->*)? (:<digits>)? ']'
354// =================================================================
355func smi_parse_bracket(
356 src: *u8, n: nx_int, pos_io: *nx_int,
357 m: *MolGraph,
358 out_z: *nx_int, out_iso: *nx_int, out_h: *nx_int, out_charge: *nx_int,
359 out_stereo: *nx_int, out_map: *nx_int, out_aromatic: *nx_int
360) -> nx_int {
361 var pos: nx_int = pos_io[0]
362 // Skip leading '['
363 pos = pos + 1
364 // ---- isotope (leading digits) ----
365 var iso: nx_int = 0
366 while pos < n {
367 let cc: nx_int = src[pos] & 0xff
368 if smi_is_digit(cc) == 0 { break }
369 iso = (iso * 10) + (cc - NX_C_0)
370 pos = pos + 1
371 }
372 if pos >= n {
373 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_UNCLOSED_BR; m.err_pos = pos
374 return 0
375 }
376 // ---- element symbol (uppercase + optional lowercase, or single lowercase for aromatic) ----
377 let c1: nx_int = src[pos] & 0xff
378 var z: nx_int = 0
379 var arom_bracket: nx_int = 0
380 var handled: nx_int = 0
381 if smi_is_upper(c1) == 1 {
382 var c2: nx_int = 0
383 if (pos + 1) < n {
384 c2 = src[pos + 1] & 0xff
385 if smi_is_lower(c2) == 0 { c2 = 0 }
386 }
387 if c2 != 0 {
388 z = smi_bracket_symbol_two_char(c1, c2)
389 if z != 0 { pos = pos + 2 }
390 }
391 if z == 0 {
392 z = smi_bracket_symbol_single_char(c1)
393 if z != 0 { pos = pos + 1 }
394 }
395 if z == 0 {
396 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_INVALID_BR; m.err_pos = pos
397 return 0
398 }
399 handled = 1
400 }
401 if handled == 0 {
402 if smi_is_lower(c1) == 1 {
403 z = smi_aromatic_lower_to_z(c1)
404 if z == 0 {
405 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_INVALID_BR; m.err_pos = pos
406 return 0
407 }
408 arom_bracket = 1
409 pos = pos + 1
410 handled = 1
411 }
412 }
413 if handled == 0 {
414 if c1 == NX_C_STAR {
415 z = 0 // wildcard atom
416 pos = pos + 1
417 handled = 1
418 }
419 }
420 if handled == 0 {
421 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos
422 return 0
423 }
424 // ---- chirality marker @ or @@ -- now POPULATED into Atom.stereo (C2.2) ----
425 var stereo_count: nx_int = 0
426 while pos < n {
427 let cc: nx_int = src[pos] & 0xff
428 if cc != NX_C_AT { break }
429 stereo_count = stereo_count + 1
430 pos = pos + 1
431 }
432 var stereo_val: nx_int = NX_STEREO_NONE
433 if stereo_count == 1 { stereo_val = NX_STEREO_CCW }
434 if stereo_count == 2 { stereo_val = NX_STEREO_CW }
435 // ---- H count: H or H<n> ----
436 // Per OpenSMILES 3.1.3: bracket atoms with no H specifier have h_count = 0
437 // (brackets are fully explicit). Default 0; bumped to 1+ if H seen.
438 var h: nx_int = 0
439 if pos < n {
440 let cc: nx_int = src[pos] & 0xff
441 if cc == NX_C_H_UP {
442 pos = pos + 1
443 h = 1 // [CH] = 1 hydrogen by default
444 if pos < n {
445 let nc: nx_int = src[pos] & 0xff
446 if smi_is_digit(nc) == 1 {
447 h = 0
448 while pos < n {
449 let dd: nx_int = src[pos] & 0xff
450 if smi_is_digit(dd) == 0 { break }
451 h = (h * 10) + (dd - NX_C_0)
452 pos = pos + 1
453 }
454 }
455 }
456 }
457 }
458 // ---- charge: + or - optionally followed by digits ----
459 var charge: nx_int = 0
460 if pos < n {
461 let cc: nx_int = src[pos] & 0xff
462 if cc == NX_C_PLUS {
463 pos = pos + 1
464 charge = 1
465 var count_extra: nx_int = 0
466 while pos < n {
467 let nc: nx_int = src[pos] & 0xff
468 if nc == NX_C_PLUS { count_extra = count_extra + 1; pos = pos + 1 }
469 else { break }
470 }
471 if count_extra > 0 { charge = charge + count_extra }
472 else {
473 if pos < n {
474 let nc2: nx_int = src[pos] & 0xff
475 if smi_is_digit(nc2) == 1 {
476 charge = 0
477 while pos < n {
478 let dd: nx_int = src[pos] & 0xff
479 if smi_is_digit(dd) == 0 { break }
480 charge = (charge * 10) + (dd - NX_C_0)
481 pos = pos + 1
482 }
483 }
484 }
485 }
486 }
487 else {
488 if cc == NX_C_MINUS {
489 pos = pos + 1
490 charge = -1
491 var count_extra2: nx_int = 0
492 while pos < n {
493 let nc: nx_int = src[pos] & 0xff
494 if nc == NX_C_MINUS { count_extra2 = count_extra2 + 1; pos = pos + 1 }
495 else { break }
496 }
497 if count_extra2 > 0 { charge = charge - count_extra2 }
498 else {
499 if pos < n {
500 let nc2: nx_int = src[pos] & 0xff
501 if smi_is_digit(nc2) == 1 {
502 var mag: nx_int = 0
503 while pos < n {
504 let dd: nx_int = src[pos] & 0xff
505 if smi_is_digit(dd) == 0 { break }
506 mag = (mag * 10) + (dd - NX_C_0)
507 pos = pos + 1
508 }
509 charge = 0 - mag
510 }
511 }
512 }
513 }
514 }
515 }
516 // ---- atom map :n -- now POPULATED into Atom.map_num (C2.2) ----
517 var map_num: nx_int = 0
518 if pos < n {
519 let cc: nx_int = src[pos] & 0xff
520 if cc == NX_C_COLON {
521 pos = pos + 1
522 while pos < n {
523 let dd: nx_int = src[pos] & 0xff
524 if smi_is_digit(dd) == 0 { break }
525 map_num = (map_num * 10) + (dd - NX_C_0)
526 pos = pos + 1
527 }
528 }
529 }
530 // ---- closing ']' ----
531 if pos >= n {
532 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_UNCLOSED_BR; m.err_pos = pos
533 return 0
534 }
535 let close_c: nx_int = src[pos] & 0xff
536 if close_c != NX_C_RBRACKET {
537 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_UNCLOSED_BR; m.err_pos = pos
538 return 0
539 }
540 pos = pos + 1
541 out_z[0] = z
542 out_iso[0] = iso
543 out_h[0] = h
544 out_charge[0] = charge
545 out_stereo[0] = stereo_val
546 out_map[0] = map_num
547 out_aromatic[0] = arom_bracket
548 pos_io[0] = pos
549 return 1
550}
551
552// =================================================================
553// Connect previous atom to new atom with pending or default bond,
554// applying pending_bstereo if set. Returns the new bond index, or -1.
555// =================================================================
556func smi_connect(m: *MolGraph, prev: nx_int, new_idx: nx_int, pending: nx_int, pending_bstereo: nx_int) -> nx_int {
557 if prev < 0 { return -1 } // no connection yet (first atom)
558 var order: nx_int = pending
559 if pending == 0 {
560 let pa: *Atom = nx_chem_mol_atom(m, prev)
561 let na: *Atom = nx_chem_mol_atom(m, new_idx)
562 if (pa as nx_int) == 0 { return -1 }
563 if (na as nx_int) == 0 { return -1 }
564 if pa.aromaticity != 0 {
565 if na.aromaticity != 0 { order = NX_BOND_AROMATIC }
566 else { order = NX_BOND_SINGLE }
567 }
568 else { order = NX_BOND_SINGLE }
569 }
570 let bi: nx_int = nx_chem_mol_bond_add(m, prev, new_idx, order)
571 if bi < 0 { return -1 }
572 if pending_bstereo != NX_BSTEREO_NONE {
573 let bd: *Bond = nx_chem_mol_bond(m, bi)
574 bd.stereo = pending_bstereo
575 }
576 return bi
577}
578
579// =================================================================
580// C2.3f: post-parse pass to populate per-stereo-atom neighbor order.
581//
582// For each Atom with stereo != 0, walks the bond list in input order
583// and records the neighbor permutation as it would be read in SMILES:
584// - If first bond touching atom is INCOMING (b.b == atom_idx): the
585// chain-prev is slot 0; implicit H (if h_count > 0) is slot 1;
586// subsequent neighbors fill slots 2-3.
587// - If first bond is OUTGOING (b.a == atom_idx; atom is root or
588// stereocenter at start of chain): implicit H (if any) is slot 0;
589// subsequent neighbors fill slots 1-3.
590//
591// Sentinel -2 marks the implicit-H position; -1 marks an unfilled slot
592// (under-determined stereo, e.g., chiral atom with <4 neighbors).
593// =================================================================
594func nx_chem_smiles_populate_stereo_neighbors(m: *MolGraph) -> nx_int {
595 var i: nx_int = 0
596 while i < m.n_atoms {
597 let a: *Atom = ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom
598 if a.stereo != NX_STEREO_NONE {
599 var has_prev: nx_int = 0
600 var first_idx: nx_int = -1
601 var bi_s: nx_int = 0
602 var found_first: nx_int = 0
603 while bi_s < m.n_bonds {
604 if found_first == 0 {
605 let b: *Bond = ((m.bonds as nx_int) + (bi_s * NX_BOND_BYTES)) as *Bond
606 var touches: nx_int = 0
607 if b.a == i { touches = 1 }
608 if b.b == i { touches = 1 }
609 if touches == 1 {
610 first_idx = bi_s
611 if b.b == i { has_prev = 1 }
612 found_first = 1
613 }
614 }
615 bi_s = bi_s + 1
616 }
617 let neighbors: *nx_int = (sys_mmap(64)) as *nx_int
618 neighbors[0] = -1
619 neighbors[1] = -1
620 neighbors[2] = -1
621 neighbors[3] = -1
622 var pos: nx_int = 0
623 if has_prev == 1 {
624 if first_idx >= 0 {
625 let bf: *Bond = ((m.bonds as nx_int) + (first_idx * NX_BOND_BYTES)) as *Bond
626 neighbors[0] = bf.a
627 pos = 1
628 }
629 }
630 if a.h_count > 0 {
631 if pos < 4 {
632 neighbors[pos] = -2
633 pos = pos + 1
634 }
635 }
636 var skipped_inc: nx_int = 0
637 var bi_w: nx_int = 0
638 while bi_w < m.n_bonds {
639 if pos < 4 {
640 let bw: *Bond = ((m.bonds as nx_int) + (bi_w * NX_BOND_BYTES)) as *Bond
641 var t_w: nx_int = 0
642 if bw.a == i { t_w = 1 }
643 if bw.b == i { t_w = 1 }
644 if t_w == 1 {
645 var skip: nx_int = 0
646 if has_prev == 1 {
647 if skipped_inc == 0 {
648 if bi_w == first_idx { skip = 1; skipped_inc = 1 }
649 }
650 }
651 if skip == 0 {
652 var other: nx_int = bw.b
653 if bw.b == i { other = bw.a }
654 neighbors[pos] = other
655 pos = pos + 1
656 }
657 }
658 }
659 bi_w = bi_w + 1
660 }
661 a.stereo_n0 = neighbors[0]
662 a.stereo_n1 = neighbors[1]
663 a.stereo_n2 = neighbors[2]
664 a.stereo_n3 = neighbors[3]
665 }
666 i = i + 1
667 }
668 return 0
669}
670
671// =================================================================
672// Main parse entry: SMILES -> MolGraph. Returns *MolGraph; check
673// .is_valid before use. On failure, .err_code names the failure
674// class and .err_pos is the offending char index.
675// =================================================================
676func nx_chem_parse_smiles(src: *u8, n: nx_int) -> *MolGraph {
677 var cap_a: nx_int = n + 4
678 var cap_b: nx_int = 2 * (n + 4)
679 let m: *MolGraph = nx_chem_mol_new(cap_a, cap_b)
680 if n <= 0 {
681 m.is_valid = 0
682 m.err_code = NX_MOL_ERR_SMILES_EMPTY
683 m.err_pos = 0
684 return m
685 }
686 // Ring closures: 100 slots (0..99) to support %nn (C2.2).
687 // -1 = unused slot; otherwise atom-index that opened the ring.
688 let ring_atom: *nx_int = (sys_mmap((100 * 8) as i64)) as *nx_int
689 let ring_bord: *nx_int = (sys_mmap((100 * 8) as i64)) as *nx_int
690 var ri: nx_int = 0
691 while ri < 100 {
692 ring_atom[ri] = -1
693 ring_bord[ri] = 0
694 ri = ri + 1
695 }
696 // Branch stack
697 let stack: *nx_int = (sys_mmap((cap_a * 8) as i64)) as *nx_int
698 var stack_n: nx_int = 0
699 var prev: nx_int = -1
700 var pending: nx_int = 0
701 var pending_bstereo: nx_int = NX_BSTEREO_NONE
702 var pos: nx_int = 0
703 var consumed: *nx_int = (sys_mmap(8)) as *nx_int
704 var iso_io: *nx_int = (sys_mmap(8)) as *nx_int
705 var z_io: *nx_int = (sys_mmap(8)) as *nx_int
706 var h_io: *nx_int = (sys_mmap(8)) as *nx_int
707 var ch_io: *nx_int = (sys_mmap(8)) as *nx_int
708 var st_io: *nx_int = (sys_mmap(8)) as *nx_int
709 var mp_io: *nx_int = (sys_mmap(8)) as *nx_int
710 var ar_io: *nx_int = (sys_mmap(8)) as *nx_int
711 while pos < n {
712 let c: nx_int = src[pos] & 0xff
713 // ---- branch open ----
714 if c == NX_C_LPAREN {
715 if prev < 0 {
716 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos
717 return m
718 }
719 stack[stack_n] = prev
720 stack_n = stack_n + 1
721 pos = pos + 1
722 continue
723 }
724 // ---- branch close ----
725 if c == NX_C_RPAREN {
726 if stack_n <= 0 {
727 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_UNCLOSED_PAREN; m.err_pos = pos
728 return m
729 }
730 stack_n = stack_n - 1
731 prev = stack[stack_n]
732 pending = 0
733 pending_bstereo = NX_BSTEREO_NONE
734 pos = pos + 1
735 continue
736 }
737 // ---- explicit bond marker ----
738 if c == NX_C_MINUS { pending = NX_BOND_SINGLE; pos = pos + 1; continue }
739 if c == NX_C_EQ { pending = NX_BOND_DOUBLE; pos = pos + 1; continue }
740 if c == NX_C_HASH { pending = NX_BOND_TRIPLE; pos = pos + 1; continue }
741 if c == NX_C_COLON { pending = NX_BOND_AROMATIC; pos = pos + 1; continue }
742 if c == NX_C_DOT { prev = -1; pending = 0; pending_bstereo = NX_BSTEREO_NONE; pos = pos + 1; continue }
743 // ---- bond stereo markers (C2.2) ----
744 if c == NX_C_SLASH { pending_bstereo = NX_BSTEREO_UP; pos = pos + 1; continue }
745 if c == NX_C_BSLASH { pending_bstereo = NX_BSTEREO_DOWN; pos = pos + 1; continue }
746 // ---- two-digit ring closure %nn (C2.2) ----
747 if c == NX_C_PCT {
748 if (pos + 2) >= n {
749 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos
750 return m
751 }
752 let d1: nx_int = src[pos + 1] & 0xff
753 let d2: nx_int = src[pos + 2] & 0xff
754 if smi_is_digit(d1) == 0 {
755 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos + 1
756 return m
757 }
758 if smi_is_digit(d2) == 0 {
759 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos + 2
760 return m
761 }
762 let d: nx_int = ((d1 - NX_C_0) * 10) + (d2 - NX_C_0)
763 if prev < 0 {
764 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_ORPHAN_RING; m.err_pos = pos
765 return m
766 }
767 if ring_atom[d] == -1 {
768 ring_atom[d] = prev
769 ring_bord[d] = pending
770 pending = 0
771 }
772 else {
773 let other: nx_int = ring_atom[d]
774 var order: nx_int = pending
775 if order == 0 { order = ring_bord[d] }
776 if order == 0 {
777 let pa: *Atom = nx_chem_mol_atom(m, other)
778 let na: *Atom = nx_chem_mol_atom(m, prev)
779 if (pa as nx_int) == 0 { return m }
780 if (na as nx_int) == 0 { return m }
781 if pa.aromaticity != 0 {
782 if na.aromaticity != 0 { order = NX_BOND_AROMATIC }
783 else { order = NX_BOND_SINGLE }
784 }
785 else { order = NX_BOND_SINGLE }
786 }
787 let bi: nx_int = nx_chem_mol_bond_add(m, prev, other, order)
788 if bi >= 0 {
789 let bd: *Bond = nx_chem_mol_bond(m, bi)
790 bd.in_ring = 1
791 if order == NX_BOND_AROMATIC {
792 bd.aromaticity = NX_AROM_DAYLIGHT
793 }
794 }
795 ring_atom[d] = -1
796 ring_bord[d] = 0
797 pending = 0
798 }
799 pos = pos + 3
800 continue
801 }
802 // ---- single-digit ring closure ----
803 if smi_is_digit(c) == 1 {
804 let d: nx_int = c - NX_C_0
805 if prev < 0 {
806 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_ORPHAN_RING; m.err_pos = pos
807 return m
808 }
809 if ring_atom[d] == -1 {
810 ring_atom[d] = prev
811 ring_bord[d] = pending
812 pending = 0
813 }
814 else {
815 let other: nx_int = ring_atom[d]
816 var order: nx_int = pending
817 if order == 0 { order = ring_bord[d] }
818 if order == 0 {
819 let pa: *Atom = nx_chem_mol_atom(m, other)
820 let na: *Atom = nx_chem_mol_atom(m, prev)
821 if (pa as nx_int) == 0 { return m }
822 if (na as nx_int) == 0 { return m }
823 if pa.aromaticity != 0 {
824 if na.aromaticity != 0 { order = NX_BOND_AROMATIC }
825 else { order = NX_BOND_SINGLE }
826 }
827 else { order = NX_BOND_SINGLE }
828 }
829 let bi: nx_int = nx_chem_mol_bond_add(m, prev, other, order)
830 if bi >= 0 {
831 let bd: *Bond = nx_chem_mol_bond(m, bi)
832 bd.in_ring = 1
833 if order == NX_BOND_AROMATIC {
834 bd.aromaticity = NX_AROM_DAYLIGHT
835 }
836 }
837 ring_atom[d] = -1
838 ring_bord[d] = 0
839 pending = 0
840 }
841 pos = pos + 1
842 continue
843 }
844 // ---- bracket atom ----
845 if c == NX_C_LBRACKET {
846 var bracket_pos: *nx_int = (sys_mmap(8)) as *nx_int
847 bracket_pos[0] = pos
848 let ok: nx_int = smi_parse_bracket(src, n, bracket_pos, m, z_io, iso_io, h_io, ch_io, st_io, mp_io, ar_io)
849 if ok == 0 { return m }
850 pos = bracket_pos[0]
851 let new_idx: nx_int = nx_chem_mol_atom_add(m, z_io[0])
852 if new_idx < 0 { return m }
853 let na: *Atom = nx_chem_mol_atom(m, new_idx)
854 na.isotope = iso_io[0]
855 na.h_count = h_io[0]
856 na.charge = ch_io[0]
857 na.stereo = st_io[0]
858 na.map_num = mp_io[0]
859 if ar_io[0] == 1 { na.aromaticity = NX_AROM_DAYLIGHT }
860 let _b: nx_int = smi_connect(m, prev, new_idx, pending, pending_bstereo)
861 prev = new_idx
862 pending = 0
863 pending_bstereo = NX_BSTEREO_NONE
864 continue
865 }
866 // ---- wildcard ----
867 if c == NX_C_STAR {
868 let new_idx: nx_int = nx_chem_mol_atom_add(m, 0)
869 if new_idx < 0 { return m }
870 let _b: nx_int = smi_connect(m, prev, new_idx, pending, pending_bstereo)
871 prev = new_idx
872 pending = 0
873 pending_bstereo = NX_BSTEREO_NONE
874 pos = pos + 1
875 continue
876 }
877 // ---- aromatic lowercase atom ----
878 let arom_z: nx_int = smi_aromatic_lower_to_z(c)
879 if arom_z > 0 {
880 let new_idx: nx_int = nx_chem_mol_atom_add(m, arom_z)
881 if new_idx < 0 { return m }
882 let na: *Atom = nx_chem_mol_atom(m, new_idx)
883 na.aromaticity = NX_AROM_DAYLIGHT
884 let _b: nx_int = smi_connect(m, prev, new_idx, pending, pending_bstereo)
885 prev = new_idx
886 pending = 0
887 pending_bstereo = NX_BSTEREO_NONE
888 pos = pos + 1
889 continue
890 }
891 // ---- organic-subset uppercase atom ----
892 if smi_is_upper(c) == 1 {
893 var c2: nx_int = 0
894 if (pos + 1) < n { c2 = src[pos + 1] & 0xff }
895 let z: nx_int = smi_organic_atom(c, c2, consumed)
896 if z > 0 {
897 let new_idx: nx_int = nx_chem_mol_atom_add(m, z)
898 if new_idx < 0 { return m }
899 let _b: nx_int = smi_connect(m, prev, new_idx, pending, pending_bstereo)
900 prev = new_idx
901 pending = 0
902 pending_bstereo = NX_BSTEREO_NONE
903 pos = pos + consumed[0]
904 continue
905 }
906 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos
907 return m
908 }
909 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_BAD_CHAR; m.err_pos = pos
910 return m
911 }
912 if stack_n != 0 {
913 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_UNCLOSED_PAREN; m.err_pos = pos
914 return m
915 }
916 var ri2: nx_int = 0
917 while ri2 < 100 {
918 if ring_atom[ri2] != -1 {
919 m.is_valid = 0; m.err_code = NX_MOL_ERR_SMILES_ORPHAN_RING; m.err_pos = pos
920 return m
921 }
922 ri2 = ri2 + 1
923 }
924 // C2.3f: populate per-stereo-atom neighbor permutation for parity correction
925 let _sp: nx_int = nx_chem_smiles_populate_stereo_neighbors(m)
926 return m
927}
928
929// =================================================================
930// Convenience: parse a NUL-terminated SMILES string.
931// =================================================================
932func nx_chem_parse_smiles_cstr(src: *u8) -> *MolGraph {
933 var n: nx_int = 0
934 let cap: nx_int = 65535
935 while n < cap {
936 let c: nx_int = src[n] & 0xff
937 if c == 0 { break }
938 n = n + 1
939 }
940 return nx_chem_parse_smiles(src, n)
941}