code wiki / (root) / nx_chem_smiles.nx

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}