nx_chem_smiles_emit.nx source
↩ module page · 945 lines · 44337 B
1// nx_chem_smiles_emit.nx -- C2.3b milestone: SMILES emit from MolGraph.
2//
3// DFS-based emit with:
4// - Organic-subset atoms emitted without brackets when possible
5// (no charge, no isotope, no stereo, no map_num, no h_count override)
6// - Bracket atoms for everything else
7// - Bond markers emitted only when non-default
8// (single between aliphatic = default; aromatic between aromatics = default)
9// - Ring closures via single digits 1..9 then %10..%99
10// - Branches via parentheses
11// - Multi-component graphs via '.' separator
12//
13// Round-trip property: parse -> emit -> parse -> emit reaches a fixed
14// point. Two different input SMILES of the same molecule do NOT yet
15// produce byte-identical output -- that requires Morgan canonical
16// ranking (C2.3c).
17//
18// Honest gaps (deferred):
19// - Morgan canonical atom ranking (C2.3c) -- unlocks bit-reproducible
20// canonical SMILES across versions/platforms (landscape EXCEED axis E1)
21// - Stereo emit (@ / @@ for atoms, / \\ for bonds) (C2.3c after canonical)
22// - Bond stereo E/Z resolution (C2.3c CIP rules)
23//
24// nx_safety_envelope:
25// intended_use: MolGraph -> SMILES string emit; round-trip clean
26// sil_target: SIL1
27// evidence: [C2.3b KAT in nx_chem_smiles_emit_test.nx; bounded buffer writes; round-trip via parse]
28// verdict: BENCH-PENDING
29
30import "nx_chem.nx"
31import "nx_chem_molecule.nx"
32import "nx_chem_smiles.nx"
33import "nx_chem_morgan.nx"
34
35const NX_MAX_BACK_PER_ATOM: nx_int = 8 // generous cap; molecules rarely > 4 ring closures per atom
36const NX_MAX_TREE_PER_ATOM: nx_int = 8 // organic atoms have <= 4 bonds typically
37
38// =================================================================
39// EmitState -- scratch storage for the DFS emit.
40// Per-atom tree-child list + per-atom back-edge digit list.
41// =================================================================
42struct EmitState {
43 n_atoms: nx_int,
44 visited: *nx_int, // size n_atoms (0/1)
45 in_dfs: *nx_int, // size n_atoms (currently on DFS stack)
46 parent: *nx_int, // size n_atoms; -1 for roots
47 // Back-edge digit registry: digit value + bond-order per (atom, slot)
48 back_digit: *nx_int, // size n_atoms * NX_MAX_BACK_PER_ATOM
49 back_bond: *nx_int,
50 back_count: *nx_int, // size n_atoms
51 next_digit: nx_int, // next free ring digit (1-99)
52 // Tree-child lists: child atom idx + bond order + bond stereo per (atom, slot)
53 tree_child: *nx_int, // size n_atoms * NX_MAX_TREE_PER_ATOM
54 tree_bond: *nx_int,
55 tree_stereo: *nx_int, // size n_atoms * NX_MAX_TREE_PER_ATOM (C2.3d)
56 tree_count: *nx_int, // size n_atoms
57}
58const NX_EMIT_STATE_BYTES: nx_int = 96
59
60// =================================================================
61// Allocate + initialize EmitState.
62// =================================================================
63func nx_chem_emit_state_new(n_atoms: nx_int) -> *EmitState {
64 let s: *EmitState = (sys_mmap(NX_EMIT_STATE_BYTES as i64)) as *EmitState
65 s.n_atoms = n_atoms
66 s.visited = (sys_mmap((n_atoms * 8) as i64)) as *nx_int
67 s.in_dfs = (sys_mmap((n_atoms * 8) as i64)) as *nx_int
68 s.parent = (sys_mmap((n_atoms * 8) as i64)) as *nx_int
69 s.back_digit = (sys_mmap((n_atoms * NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int
70 s.back_bond = (sys_mmap((n_atoms * NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int
71 s.back_count = (sys_mmap((n_atoms * 8) as i64)) as *nx_int
72 s.next_digit = 1
73 s.tree_child = (sys_mmap((n_atoms * NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int
74 s.tree_bond = (sys_mmap((n_atoms * NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int
75 s.tree_stereo = (sys_mmap((n_atoms * NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int
76 s.tree_count = (sys_mmap((n_atoms * 8) as i64)) as *nx_int
77 // mmap pages are zero -- parent[i] = 0 will need explicit -1 init
78 var i: nx_int = 0
79 while i < n_atoms {
80 s.parent[i] = -1
81 i = i + 1
82 }
83 return s
84}
85
86// =================================================================
87// Bounded byte append. Returns 0 on success, -1 on buffer overflow.
88// =================================================================
89func nx_chem_emit_byte(buf: *u8, len_io: *nx_int, cap: nx_int, b: nx_int) -> nx_int {
90 if len_io[0] >= cap { return -1 }
91 buf[len_io[0]] = b & 0xff
92 len_io[0] = len_io[0] + 1
93 return 0
94}
95
96// =================================================================
97// Emit unsigned integer in decimal.
98// =================================================================
99func nx_chem_emit_uint(buf: *u8, len_io: *nx_int, cap: nx_int, v: nx_int) -> nx_int {
100 if v == 0 { return nx_chem_emit_byte(buf, len_io, cap, 0x30) }
101 // up to 10 digits for 32-bit-range; in practice ring digits + isotopes
102 // + charges are <= 3 digits
103 let tmp: *u8 = (sys_mmap(16)) as *u8
104 var n: nx_int = 0
105 var x: nx_int = v
106 while x > 0 {
107 tmp[n] = ((x - ((x / 10) * 10)) + 0x30) & 0xff
108 x = x / 10
109 n = n + 1
110 }
111 var i: nx_int = n - 1
112 while i >= 0 {
113 let r: nx_int = nx_chem_emit_byte(buf, len_io, cap, tmp[i] & 0xff)
114 if r != 0 { return r }
115 i = i - 1
116 }
117 return 0
118}
119
120// =================================================================
121// Emit a ring-closure digit. Uses 1..9 directly, %10..%99 with the
122// % prefix for two-digit closures.
123// =================================================================
124func nx_chem_emit_ring_digit(buf: *u8, len_io: *nx_int, cap: nx_int, d: nx_int) -> nx_int {
125 if d < 10 {
126 return nx_chem_emit_byte(buf, len_io, cap, 0x30 + d)
127 }
128 let r1: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_PCT)
129 if r1 != 0 { return r1 }
130 return nx_chem_emit_uint(buf, len_io, cap, d)
131}
132
133// =================================================================
134// Emit a bond marker. Omits the marker when default (single between
135// non-aromatic atoms, aromatic between two aromatic atoms).
136//
137// pa_arom + nb_arom non-zero = both endpoints aromatic.
138// =================================================================
139func nx_chem_emit_bond_marker(buf: *u8, len_io: *nx_int, cap: nx_int, order: nx_int, both_aromatic: nx_int, stereo: nx_int) -> nx_int {
140 // Bond stereo (/ or \) for single bonds adjacent to a double bond.
141 // C2.3d: emit stored stereo verbatim; parity correction for cases
142 // where canonical DFS reverses traversal vs input is deferred to C2.3e.
143 if stereo == NX_BSTEREO_UP {
144 return nx_chem_emit_byte(buf, len_io, cap, NX_C_SLASH)
145 }
146 if stereo == NX_BSTEREO_DOWN {
147 return nx_chem_emit_byte(buf, len_io, cap, NX_C_BSLASH)
148 }
149 if order == NX_BOND_SINGLE {
150 // single bond is the default for aliphatic atoms; omit.
151 return 0
152 }
153 if order == NX_BOND_AROMATIC {
154 // aromatic bond by definition connects two aromatic atoms; omit
155 return 0
156 }
157 if order == NX_BOND_DOUBLE { return nx_chem_emit_byte(buf, len_io, cap, NX_C_EQ) }
158 if order == NX_BOND_TRIPLE { return nx_chem_emit_byte(buf, len_io, cap, NX_C_HASH) }
159 if order == NX_BOND_ZERO { return nx_chem_emit_byte(buf, len_io, cap, NX_C_MINUS) }
160 return 0
161}
162
163// =================================================================
164// Z -> organic-subset uppercase symbol (1 or 2 chars).
165// Returns symbol length on match (1 or 2), 0 if z not in organic subset.
166// =================================================================
167func nx_chem_z_to_organic_aliphatic(z: nx_int, c1_out: *nx_int, c2_out: *nx_int) -> nx_int {
168 if z == 5 { c1_out[0] = 0x42; c2_out[0] = 0; return 1 } // B
169 if z == 6 { c1_out[0] = 0x43; c2_out[0] = 0; return 1 } // C
170 if z == 7 { c1_out[0] = 0x4E; c2_out[0] = 0; return 1 } // N
171 if z == 8 { c1_out[0] = 0x4F; c2_out[0] = 0; return 1 } // O
172 if z == 9 { c1_out[0] = 0x46; c2_out[0] = 0; return 1 } // F
173 if z == 15 { c1_out[0] = 0x50; c2_out[0] = 0; return 1 } // P
174 if z == 16 { c1_out[0] = 0x53; c2_out[0] = 0; return 1 } // S
175 if z == 17 { c1_out[0] = 0x43; c2_out[0] = 0x6C; return 2 } // Cl
176 if z == 35 { c1_out[0] = 0x42; c2_out[0] = 0x72; return 2 } // Br
177 if z == 53 { c1_out[0] = 0x49; c2_out[0] = 0; return 1 } // I
178 return 0
179}
180
181// =================================================================
182// Z -> aromatic lowercase symbol (single char).
183// =================================================================
184func nx_chem_z_to_aromatic_lower(z: nx_int) -> nx_int {
185 if z == 5 { return 0x62 } // b
186 if z == 6 { return 0x63 } // c
187 if z == 7 { return 0x6E } // n
188 if z == 8 { return 0x6F } // o
189 if z == 15 { return 0x70 } // p
190 if z == 16 { return 0x73 } // s
191 return 0
192}
193
194// =================================================================
195// Z -> any-element symbol for bracket atoms (1 or 2 chars).
196// Covers all 118 elements. Returns symbol length.
197// =================================================================
198func nx_chem_z_to_bracket_symbol(z: nx_int, c1_out: *nx_int, c2_out: *nx_int) -> nx_int {
199 // Single-char first
200 if z == 1 { c1_out[0] = 0x48; c2_out[0] = 0; return 1 } // H
201 if z == 5 { c1_out[0] = 0x42; c2_out[0] = 0; return 1 } // B
202 if z == 6 { c1_out[0] = 0x43; c2_out[0] = 0; return 1 } // C
203 if z == 7 { c1_out[0] = 0x4E; c2_out[0] = 0; return 1 } // N
204 if z == 8 { c1_out[0] = 0x4F; c2_out[0] = 0; return 1 } // O
205 if z == 9 { c1_out[0] = 0x46; c2_out[0] = 0; return 1 } // F
206 if z == 15 { c1_out[0] = 0x50; c2_out[0] = 0; return 1 } // P
207 if z == 16 { c1_out[0] = 0x53; c2_out[0] = 0; return 1 } // S
208 if z == 19 { c1_out[0] = 0x4B; c2_out[0] = 0; return 1 } // K
209 if z == 23 { c1_out[0] = 0x56; c2_out[0] = 0; return 1 } // V
210 if z == 39 { c1_out[0] = 0x59; c2_out[0] = 0; return 1 } // Y
211 if z == 53 { c1_out[0] = 0x49; c2_out[0] = 0; return 1 } // I
212 if z == 74 { c1_out[0] = 0x57; c2_out[0] = 0; return 1 } // W
213 if z == 92 { c1_out[0] = 0x55; c2_out[0] = 0; return 1 } // U
214 // Two-char (large switch keyed by z)
215 if z == 2 { c1_out[0] = 0x48; c2_out[0] = 0x65; return 2 } // He
216 if z == 3 { c1_out[0] = 0x4C; c2_out[0] = 0x69; return 2 } // Li
217 if z == 4 { c1_out[0] = 0x42; c2_out[0] = 0x65; return 2 } // Be
218 if z == 10 { c1_out[0] = 0x4E; c2_out[0] = 0x65; return 2 } // Ne
219 if z == 11 { c1_out[0] = 0x4E; c2_out[0] = 0x61; return 2 } // Na
220 if z == 12 { c1_out[0] = 0x4D; c2_out[0] = 0x67; return 2 } // Mg
221 if z == 13 { c1_out[0] = 0x41; c2_out[0] = 0x6C; return 2 } // Al
222 if z == 14 { c1_out[0] = 0x53; c2_out[0] = 0x69; return 2 } // Si
223 if z == 17 { c1_out[0] = 0x43; c2_out[0] = 0x6C; return 2 } // Cl
224 if z == 18 { c1_out[0] = 0x41; c2_out[0] = 0x72; return 2 } // Ar
225 if z == 20 { c1_out[0] = 0x43; c2_out[0] = 0x61; return 2 } // Ca
226 if z == 21 { c1_out[0] = 0x53; c2_out[0] = 0x63; return 2 } // Sc
227 if z == 22 { c1_out[0] = 0x54; c2_out[0] = 0x69; return 2 } // Ti
228 if z == 24 { c1_out[0] = 0x43; c2_out[0] = 0x72; return 2 } // Cr
229 if z == 25 { c1_out[0] = 0x4D; c2_out[0] = 0x6E; return 2 } // Mn
230 if z == 26 { c1_out[0] = 0x46; c2_out[0] = 0x65; return 2 } // Fe
231 if z == 27 { c1_out[0] = 0x43; c2_out[0] = 0x6F; return 2 } // Co
232 if z == 28 { c1_out[0] = 0x4E; c2_out[0] = 0x69; return 2 } // Ni
233 if z == 29 { c1_out[0] = 0x43; c2_out[0] = 0x75; return 2 } // Cu
234 if z == 30 { c1_out[0] = 0x5A; c2_out[0] = 0x6E; return 2 } // Zn
235 if z == 31 { c1_out[0] = 0x47; c2_out[0] = 0x61; return 2 } // Ga
236 if z == 32 { c1_out[0] = 0x47; c2_out[0] = 0x65; return 2 } // Ge
237 if z == 33 { c1_out[0] = 0x41; c2_out[0] = 0x73; return 2 } // As
238 if z == 34 { c1_out[0] = 0x53; c2_out[0] = 0x65; return 2 } // Se
239 if z == 35 { c1_out[0] = 0x42; c2_out[0] = 0x72; return 2 } // Br
240 if z == 36 { c1_out[0] = 0x4B; c2_out[0] = 0x72; return 2 } // Kr
241 if z == 37 { c1_out[0] = 0x52; c2_out[0] = 0x62; return 2 } // Rb
242 if z == 38 { c1_out[0] = 0x53; c2_out[0] = 0x72; return 2 } // Sr
243 if z == 40 { c1_out[0] = 0x5A; c2_out[0] = 0x72; return 2 } // Zr
244 if z == 41 { c1_out[0] = 0x4E; c2_out[0] = 0x62; return 2 } // Nb
245 if z == 42 { c1_out[0] = 0x4D; c2_out[0] = 0x6F; return 2 } // Mo
246 if z == 43 { c1_out[0] = 0x54; c2_out[0] = 0x63; return 2 } // Tc
247 if z == 44 { c1_out[0] = 0x52; c2_out[0] = 0x75; return 2 } // Ru
248 if z == 45 { c1_out[0] = 0x52; c2_out[0] = 0x68; return 2 } // Rh
249 if z == 46 { c1_out[0] = 0x50; c2_out[0] = 0x64; return 2 } // Pd
250 if z == 47 { c1_out[0] = 0x41; c2_out[0] = 0x67; return 2 } // Ag
251 if z == 48 { c1_out[0] = 0x43; c2_out[0] = 0x64; return 2 } // Cd
252 if z == 49 { c1_out[0] = 0x49; c2_out[0] = 0x6E; return 2 } // In
253 if z == 50 { c1_out[0] = 0x53; c2_out[0] = 0x6E; return 2 } // Sn
254 if z == 51 { c1_out[0] = 0x53; c2_out[0] = 0x62; return 2 } // Sb
255 if z == 52 { c1_out[0] = 0x54; c2_out[0] = 0x65; return 2 } // Te
256 if z == 54 { c1_out[0] = 0x58; c2_out[0] = 0x65; return 2 } // Xe
257 if z == 55 { c1_out[0] = 0x43; c2_out[0] = 0x73; return 2 } // Cs
258 if z == 56 { c1_out[0] = 0x42; c2_out[0] = 0x61; return 2 } // Ba
259 if z == 57 { c1_out[0] = 0x4C; c2_out[0] = 0x61; return 2 } // La
260 if z == 58 { c1_out[0] = 0x43; c2_out[0] = 0x65; return 2 } // Ce
261 if z == 59 { c1_out[0] = 0x50; c2_out[0] = 0x72; return 2 } // Pr
262 if z == 60 { c1_out[0] = 0x4E; c2_out[0] = 0x64; return 2 } // Nd
263 if z == 61 { c1_out[0] = 0x50; c2_out[0] = 0x6D; return 2 } // Pm
264 if z == 62 { c1_out[0] = 0x53; c2_out[0] = 0x6D; return 2 } // Sm
265 if z == 63 { c1_out[0] = 0x45; c2_out[0] = 0x75; return 2 } // Eu
266 if z == 64 { c1_out[0] = 0x47; c2_out[0] = 0x64; return 2 } // Gd
267 if z == 65 { c1_out[0] = 0x54; c2_out[0] = 0x62; return 2 } // Tb
268 if z == 66 { c1_out[0] = 0x44; c2_out[0] = 0x79; return 2 } // Dy
269 if z == 67 { c1_out[0] = 0x48; c2_out[0] = 0x6F; return 2 } // Ho
270 if z == 68 { c1_out[0] = 0x45; c2_out[0] = 0x72; return 2 } // Er
271 if z == 69 { c1_out[0] = 0x54; c2_out[0] = 0x6D; return 2 } // Tm
272 if z == 70 { c1_out[0] = 0x59; c2_out[0] = 0x62; return 2 } // Yb
273 if z == 71 { c1_out[0] = 0x4C; c2_out[0] = 0x75; return 2 } // Lu
274 if z == 72 { c1_out[0] = 0x48; c2_out[0] = 0x66; return 2 } // Hf
275 if z == 73 { c1_out[0] = 0x54; c2_out[0] = 0x61; return 2 } // Ta
276 if z == 75 { c1_out[0] = 0x52; c2_out[0] = 0x65; return 2 } // Re
277 if z == 76 { c1_out[0] = 0x4F; c2_out[0] = 0x73; return 2 } // Os
278 if z == 77 { c1_out[0] = 0x49; c2_out[0] = 0x72; return 2 } // Ir
279 if z == 78 { c1_out[0] = 0x50; c2_out[0] = 0x74; return 2 } // Pt
280 if z == 79 { c1_out[0] = 0x41; c2_out[0] = 0x75; return 2 } // Au
281 if z == 80 { c1_out[0] = 0x48; c2_out[0] = 0x67; return 2 } // Hg
282 if z == 81 { c1_out[0] = 0x54; c2_out[0] = 0x6C; return 2 } // Tl
283 if z == 82 { c1_out[0] = 0x50; c2_out[0] = 0x62; return 2 } // Pb
284 if z == 83 { c1_out[0] = 0x42; c2_out[0] = 0x69; return 2 } // Bi
285 if z == 84 { c1_out[0] = 0x50; c2_out[0] = 0x6F; return 2 } // Po
286 if z == 85 { c1_out[0] = 0x41; c2_out[0] = 0x74; return 2 } // At
287 if z == 86 { c1_out[0] = 0x52; c2_out[0] = 0x6E; return 2 } // Rn
288 if z == 87 { c1_out[0] = 0x46; c2_out[0] = 0x72; return 2 } // Fr
289 if z == 88 { c1_out[0] = 0x52; c2_out[0] = 0x61; return 2 } // Ra
290 if z == 89 { c1_out[0] = 0x41; c2_out[0] = 0x63; return 2 } // Ac
291 if z == 90 { c1_out[0] = 0x54; c2_out[0] = 0x68; return 2 } // Th
292 if z == 91 { c1_out[0] = 0x50; c2_out[0] = 0x61; return 2 } // Pa
293 if z == 93 { c1_out[0] = 0x4E; c2_out[0] = 0x70; return 2 } // Np
294 if z == 94 { c1_out[0] = 0x50; c2_out[0] = 0x75; return 2 } // Pu
295 if z == 95 { c1_out[0] = 0x41; c2_out[0] = 0x6D; return 2 } // Am
296 if z == 96 { c1_out[0] = 0x43; c2_out[0] = 0x6D; return 2 } // Cm
297 if z == 97 { c1_out[0] = 0x42; c2_out[0] = 0x6B; return 2 } // Bk
298 if z == 98 { c1_out[0] = 0x43; c2_out[0] = 0x66; return 2 } // Cf
299 if z == 99 { c1_out[0] = 0x45; c2_out[0] = 0x73; return 2 } // Es
300 if z == 100 { c1_out[0] = 0x46; c2_out[0] = 0x6D; return 2 } // Fm
301 if z == 101 { c1_out[0] = 0x4D; c2_out[0] = 0x64; return 2 } // Md
302 if z == 102 { c1_out[0] = 0x4E; c2_out[0] = 0x6F; return 2 } // No
303 if z == 103 { c1_out[0] = 0x4C; c2_out[0] = 0x72; return 2 } // Lr
304 if z == 104 { c1_out[0] = 0x52; c2_out[0] = 0x66; return 2 } // Rf
305 if z == 105 { c1_out[0] = 0x44; c2_out[0] = 0x62; return 2 } // Db
306 if z == 106 { c1_out[0] = 0x53; c2_out[0] = 0x67; return 2 } // Sg
307 if z == 107 { c1_out[0] = 0x42; c2_out[0] = 0x68; return 2 } // Bh
308 if z == 108 { c1_out[0] = 0x48; c2_out[0] = 0x73; return 2 } // Hs
309 if z == 109 { c1_out[0] = 0x4D; c2_out[0] = 0x74; return 2 } // Mt
310 if z == 110 { c1_out[0] = 0x44; c2_out[0] = 0x73; return 2 } // Ds
311 if z == 111 { c1_out[0] = 0x52; c2_out[0] = 0x67; return 2 } // Rg
312 if z == 112 { c1_out[0] = 0x43; c2_out[0] = 0x6E; return 2 } // Cn
313 if z == 113 { c1_out[0] = 0x4E; c2_out[0] = 0x68; return 2 } // Nh
314 if z == 114 { c1_out[0] = 0x46; c2_out[0] = 0x6C; return 2 } // Fl
315 if z == 115 { c1_out[0] = 0x4D; c2_out[0] = 0x63; return 2 } // Mc
316 if z == 116 { c1_out[0] = 0x4C; c2_out[0] = 0x76; return 2 } // Lv
317 if z == 117 { c1_out[0] = 0x54; c2_out[0] = 0x73; return 2 } // Ts
318 if z == 118 { c1_out[0] = 0x4F; c2_out[0] = 0x67; return 2 } // Og
319 return 0
320}
321
322// =================================================================
323// Decide whether an atom needs brackets. Returns 1 if brackets
324// required, 0 if can be emitted without brackets.
325//
326// Rules:
327// - z = 0 (wildcard *) -> no brackets, emit "*"
328// - charge != 0 -> brackets
329// - isotope != 0 -> brackets
330// - stereo != 0 -> brackets (so the @/@@ emit has a frame)
331// - map_num != 0 -> brackets
332// - z not in organic subset (B/C/N/O/P/S/F/Cl/Br/I) -> brackets
333// - z in organic subset, aromaticity != 0 -> no brackets (lowercase)
334// - z in organic subset, aliphatic -> no brackets (uppercase)
335// =================================================================
336func nx_chem_atom_needs_brackets(a: *Atom) -> nx_int {
337 if a.z == 0 { return 0 }
338 if a.charge != 0 { return 1 }
339 if a.isotope != 0 { return 1 }
340 if a.stereo != 0 { return 1 }
341 if a.map_num != 0 { return 1 }
342 // organic subset check
343 if a.z == 5 { return 0 }
344 if a.z == 6 { return 0 }
345 if a.z == 7 { return 0 }
346 if a.z == 8 { return 0 }
347 if a.z == 9 { return 0 }
348 if a.z == 15 { return 0 }
349 if a.z == 16 { return 0 }
350 if a.z == 17 { return 0 }
351 if a.z == 35 { return 0 }
352 if a.z == 53 { return 0 }
353 return 1
354}
355
356// =================================================================
357// Emit a single atom. Brackets when needed; bare symbol otherwise.
358// =================================================================
359// C2.3f: emit_atom with parity-corrected stereo override.
360// stereo_override: -1 means use a.stereo verbatim; >=0 means use this value instead.
361func nx_chem_emit_atom_with_stereo(buf: *u8, len_io: *nx_int, cap: nx_int, a: *Atom, stereo_override: nx_int) -> nx_int {
362 var effective_stereo: nx_int = a.stereo
363 if stereo_override >= 0 { effective_stereo = stereo_override }
364 // Wildcard
365 if a.z == 0 {
366 return nx_chem_emit_byte(buf, len_io, cap, NX_C_STAR)
367 }
368 let c1_io: *nx_int = (sys_mmap(8)) as *nx_int
369 let c2_io: *nx_int = (sys_mmap(8)) as *nx_int
370 let needs_br: nx_int = nx_chem_atom_needs_brackets(a)
371 if needs_br == 0 {
372 // Aromatic lowercase if aromatic, else aliphatic uppercase
373 if a.aromaticity != 0 {
374 let alc: nx_int = nx_chem_z_to_aromatic_lower(a.z)
375 if alc > 0 {
376 return nx_chem_emit_byte(buf, len_io, cap, alc)
377 }
378 // aromatic but not lowercase-representable (e.g. Cl in aromatic context — rare); fall through to brackets
379 }
380 let alen: nx_int = nx_chem_z_to_organic_aliphatic(a.z, c1_io, c2_io)
381 if alen >= 1 {
382 let r1: nx_int = nx_chem_emit_byte(buf, len_io, cap, c1_io[0])
383 if r1 != 0 { return r1 }
384 if alen == 2 {
385 let r2: nx_int = nx_chem_emit_byte(buf, len_io, cap, c2_io[0])
386 if r2 != 0 { return r2 }
387 }
388 return 0
389 }
390 }
391 // Bracket emit
392 let r_l: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_LBRACKET)
393 if r_l != 0 { return r_l }
394 if a.isotope != 0 {
395 let r_i: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.isotope)
396 if r_i != 0 { return r_i }
397 }
398 let slen: nx_int = nx_chem_z_to_bracket_symbol(a.z, c1_io, c2_io)
399 if slen == 0 {
400 // Unknown element; emit "*" as fallback inside brackets
401 let r_star: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_STAR)
402 if r_star != 0 { return r_star }
403 }
404 else {
405 let r_c1: nx_int = nx_chem_emit_byte(buf, len_io, cap, c1_io[0])
406 if r_c1 != 0 { return r_c1 }
407 if slen == 2 {
408 let r_c2: nx_int = nx_chem_emit_byte(buf, len_io, cap, c2_io[0])
409 if r_c2 != 0 { return r_c2 }
410 }
411 }
412 // stereo @ (C2.3f: parity-corrected via effective_stereo)
413 if effective_stereo == NX_STEREO_CCW {
414 let r_at: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_AT)
415 if r_at != 0 { return r_at }
416 }
417 if effective_stereo == NX_STEREO_CW {
418 let r_at1: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_AT)
419 if r_at1 != 0 { return r_at1 }
420 let r_at2: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_AT)
421 if r_at2 != 0 { return r_at2 }
422 }
423 // H count
424 if a.h_count > 0 {
425 let r_h: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_H_UP)
426 if r_h != 0 { return r_h }
427 if a.h_count > 1 {
428 let r_hn: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.h_count)
429 if r_hn != 0 { return r_hn }
430 }
431 }
432 // charge
433 if a.charge > 0 {
434 let r_pl: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_PLUS)
435 if r_pl != 0 { return r_pl }
436 if a.charge > 1 {
437 let r_pn: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.charge)
438 if r_pn != 0 { return r_pn }
439 }
440 }
441 if a.charge < 0 {
442 let r_mi: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_MINUS)
443 if r_mi != 0 { return r_mi }
444 let mag: nx_int = 0 - a.charge
445 if mag > 1 {
446 let r_mn: nx_int = nx_chem_emit_uint(buf, len_io, cap, mag)
447 if r_mn != 0 { return r_mn }
448 }
449 }
450 // atom map
451 if a.map_num != 0 {
452 let r_co: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_COLON)
453 if r_co != 0 { return r_co }
454 let r_mp: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.map_num)
455 if r_mp != 0 { return r_mp }
456 }
457 return nx_chem_emit_byte(buf, len_io, cap, NX_C_RBRACKET)
458}
459
460// =================================================================
461// Wrapper for back-compat: emit atom with default (no override).
462// =================================================================
463func nx_chem_emit_atom(buf: *u8, len_io: *nx_int, cap: nx_int, a: *Atom) -> nx_int {
464 return nx_chem_emit_atom_with_stereo(buf, len_io, cap, a, -1)
465}
466
467// =================================================================
468// Pre-scan DFS: identify tree edges + back-edges + assign ring digits.
469//
470// Uses an explicit stack to avoid recursion depth issues. For each
471// atom processed:
472// - mark visited
473// - enumerate incident edges
474// - for each neighbor:
475// if neighbor visited && neighbor != parent && not already recorded:
476// allocate next ring digit; record on BOTH endpoints
477// else if neighbor not visited:
478// add as tree child + push onto DFS stack
479// =================================================================
480func nx_chem_emit_prescan(m: *MolGraph, s: *EmitState, root: nx_int) -> nx_int {
481 // explicit DFS stack of atom indices
482 let stack: *nx_int = (sys_mmap((s.n_atoms * 8) as i64)) as *nx_int
483 var stack_n: nx_int = 0
484 stack[stack_n] = root
485 stack_n = stack_n + 1
486 s.visited[root] = 1
487 while stack_n > 0 {
488 stack_n = stack_n - 1
489 let u: nx_int = stack[stack_n]
490 // enumerate incident edges
491 var ei: nx_int = 0
492 while ei < m.n_bonds {
493 let b: *Bond = ((m.bonds as nx_int) + (ei * NX_BOND_BYTES)) as *Bond
494 var v: nx_int = -1
495 if b.a == u { v = b.b }
496 if b.b == u { v = b.a }
497 if v >= 0 {
498 if v != s.parent[u] {
499 if s.visited[v] == 1 {
500 // back-edge u-v; record only once (when u > v)
501 if u > v {
502 // assign next digit
503 let d: nx_int = s.next_digit
504 s.next_digit = s.next_digit + 1
505 // record on u
506 let cu: nx_int = s.back_count[u]
507 if cu < NX_MAX_BACK_PER_ATOM {
508 s.back_digit[(u * NX_MAX_BACK_PER_ATOM) + cu] = d
509 s.back_bond[(u * NX_MAX_BACK_PER_ATOM) + cu] = b.order
510 s.back_count[u] = cu + 1
511 }
512 // record on v
513 let cv: nx_int = s.back_count[v]
514 if cv < NX_MAX_BACK_PER_ATOM {
515 s.back_digit[(v * NX_MAX_BACK_PER_ATOM) + cv] = d
516 s.back_bond[(v * NX_MAX_BACK_PER_ATOM) + cv] = b.order
517 s.back_count[v] = cv + 1
518 }
519 }
520 }
521 else {
522 // tree edge u -> v
523 // C2.3e: flip bond stereo if DFS direction (u->v) is reversed
524 // relative to bond's stored (a->b) direction. UP <-> DOWN keeps
525 // cis/trans semantics intact.
526 var bond_stereo: nx_int = b.stereo
527 if u == b.b {
528 if bond_stereo == NX_BSTEREO_UP { bond_stereo = NX_BSTEREO_DOWN }
529 else {
530 if bond_stereo == NX_BSTEREO_DOWN { bond_stereo = NX_BSTEREO_UP }
531 }
532 }
533 let ct: nx_int = s.tree_count[u]
534 if ct < NX_MAX_TREE_PER_ATOM {
535 s.tree_child[(u * NX_MAX_TREE_PER_ATOM) + ct] = v
536 s.tree_bond[(u * NX_MAX_TREE_PER_ATOM) + ct] = b.order
537 s.tree_stereo[(u * NX_MAX_TREE_PER_ATOM) + ct] = bond_stereo
538 s.tree_count[u] = ct + 1
539 }
540 s.visited[v] = 1
541 s.parent[v] = u
542 // push v on stack (LIFO: depth-first; but for stable emit-order
543 // we want children visited in bond-index order, so we push in REVERSE order)
544 // For now, simple push; this affects branch ordering but not correctness.
545 stack[stack_n] = v
546 stack_n = stack_n + 1
547 }
548 }
549 }
550 ei = ei + 1
551 }
552 }
553 return 0
554}
555
556// =================================================================
557// Emit a single DFS subtree rooted at atom_idx. bond_in is the bond
558// order of the edge from parent to atom_idx (or NX_BOND_SINGLE for
559// the root since that bond marker is omitted by default rules anyway).
560//
561// from_aromatic = 1 if the parent atom was aromatic (used to decide
562// whether to omit aromatic bond marker).
563// =================================================================
564
565// =================================================================
566// C2.3f: compute parity-corrected stereo for a chiral atom at emit time.
567//
568// Returns:
569// -1 if atom has no stereo or correction can't be determined (passes
570// through emit_atom which uses a.stereo verbatim)
571// NX_STEREO_CCW or NX_STEREO_CW: the corrected marker for canonical emit
572//
573// Algorithm:
574// 1. Build input order from a.stereo_n0..n3 (set at parse time).
575// 2. Build emit order: parent (if any) + implicit H (if h_count > 0) +
576// ring closures + tree children. For MVP we skip atoms with
577// ring closures (back_count > 0).
578// 3. Compute permutation parity (count inversions) between input
579// order and emit order.
580// 4. If parity is even, return a.stereo unchanged.
581// If parity is odd, return flipped (CCW <-> CW).
582// =================================================================
583func nx_chem_compute_stereo_parity(s: *EmitState, atom_idx: nx_int, a: *Atom) -> nx_int {
584 if a.stereo == NX_STEREO_NONE { return -1 }
585 // Verify stereo_n* is fully populated (parse-time may leave -1 if
586 // atom had fewer than expected neighbors).
587 if a.stereo_n3 == -1 { return -1 }
588 // MVP: skip atoms with ring closures (back-edges). They need their
589 // own neighbor-order computation (back-edges interleave with tree
590 // children at the bracket).
591 if s.back_count[atom_idx] > 0 { return -1 }
592 // Build emit order
593 let emit_order: *nx_int = (sys_mmap(64)) as *nx_int
594 emit_order[0] = -1
595 emit_order[1] = -1
596 emit_order[2] = -1
597 emit_order[3] = -1
598 var pos: nx_int = 0
599 let parent: nx_int = s.parent[atom_idx]
600 if parent >= 0 {
601 emit_order[pos] = parent
602 pos = pos + 1
603 }
604 if a.h_count > 0 {
605 if pos < 4 {
606 emit_order[pos] = -2
607 pos = pos + 1
608 }
609 }
610 // tree children in tree_child order (rank-sorted by canonical prescan)
611 var ti: nx_int = 0
612 while ti < s.tree_count[atom_idx] {
613 if pos < 4 {
614 emit_order[pos] = s.tree_child[(atom_idx * NX_MAX_TREE_PER_ATOM) + ti]
615 pos = pos + 1
616 }
617 ti = ti + 1
618 }
619 if pos != 4 { return -1 }
620 // Compute permutation map: for each emit[i], find position in input
621 let input_order: *nx_int = (sys_mmap(64)) as *nx_int
622 input_order[0] = a.stereo_n0
623 input_order[1] = a.stereo_n1
624 input_order[2] = a.stereo_n2
625 input_order[3] = a.stereo_n3
626 let map: *nx_int = (sys_mmap(64)) as *nx_int
627 var mi: nx_int = 0
628 while mi < 4 {
629 var mj: nx_int = 0
630 var found: nx_int = -1
631 while mj < 4 {
632 if input_order[mj] == emit_order[mi] { found = mj }
633 mj = mj + 1
634 }
635 if found < 0 { return -1 } // emit element not found in input -> can't determine parity
636 map[mi] = found
637 mi = mi + 1
638 }
639 // Count inversions in map
640 var inv: nx_int = 0
641 var pi: nx_int = 0
642 while pi < 4 {
643 var pj: nx_int = pi + 1
644 while pj < 4 {
645 if map[pi] > map[pj] { inv = inv + 1 }
646 pj = pj + 1
647 }
648 pi = pi + 1
649 }
650 // Parity: even -> keep, odd -> flip
651 let odd: nx_int = inv & 1
652 if odd == 0 { return a.stereo }
653 if a.stereo == NX_STEREO_CCW { return NX_STEREO_CW }
654 if a.stereo == NX_STEREO_CW { return NX_STEREO_CCW }
655 return a.stereo
656}
657
658func nx_chem_emit_subtree(
659 buf: *u8, len_io: *nx_int, cap: nx_int,
660 m: *MolGraph, s: *EmitState,
661 atom_idx: nx_int, bond_in: nx_int, bond_in_stereo: nx_int, from_aromatic: nx_int
662) -> nx_int {
663 let a: *Atom = ((m.atoms as nx_int) + (atom_idx * NX_ATOM_BYTES)) as *Atom
664 var both_arom: nx_int = 0
665 if from_aromatic != 0 {
666 if a.aromaticity != 0 { both_arom = 1 }
667 }
668 let r_bm: nx_int = nx_chem_emit_bond_marker(buf, len_io, cap, bond_in, both_arom, bond_in_stereo)
669 if r_bm != 0 { return r_bm }
670 // emit atom (C2.3f: compute parity-corrected stereo for chiral centers)
671 let corrected_stereo: nx_int = nx_chem_compute_stereo_parity(s, atom_idx, a)
672 let r_at: nx_int = nx_chem_emit_atom_with_stereo(buf, len_io, cap, a, corrected_stereo)
673 if r_at != 0 { return r_at }
674 // emit ring closures attached to this atom (back-edge bonds have no stereo in C2.3d)
675 var bi: nx_int = 0
676 while bi < s.back_count[atom_idx] {
677 let d: nx_int = s.back_digit[(atom_idx * NX_MAX_BACK_PER_ATOM) + bi]
678 let bo: nx_int = s.back_bond[(atom_idx * NX_MAX_BACK_PER_ATOM) + bi]
679 let r_bbm: nx_int = nx_chem_emit_bond_marker(buf, len_io, cap, bo, both_arom, NX_BSTEREO_NONE)
680 if r_bbm != 0 { return r_bbm }
681 let r_rd: nx_int = nx_chem_emit_ring_digit(buf, len_io, cap, d)
682 if r_rd != 0 { return r_rd }
683 bi = bi + 1
684 }
685 // recurse into tree children; all but last are branches in parens
686 let n_children: nx_int = s.tree_count[atom_idx]
687 var ci: nx_int = 0
688 while ci < n_children {
689 let child: nx_int = s.tree_child[(atom_idx * NX_MAX_TREE_PER_ATOM) + ci]
690 let cbond: nx_int = s.tree_bond[(atom_idx * NX_MAX_TREE_PER_ATOM) + ci]
691 let cstereo: nx_int = s.tree_stereo[(atom_idx * NX_MAX_TREE_PER_ATOM) + ci]
692 let is_last: nx_int = ci == (n_children - 1)
693 if is_last == 1 {
694 let r_sub: nx_int = nx_chem_emit_subtree(buf, len_io, cap, m, s, child, cbond, cstereo, a.aromaticity)
695 if r_sub != 0 { return r_sub }
696 }
697 else {
698 let r_lp: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_LPAREN)
699 if r_lp != 0 { return r_lp }
700 let r_sub: nx_int = nx_chem_emit_subtree(buf, len_io, cap, m, s, child, cbond, cstereo, a.aromaticity)
701 if r_sub != 0 { return r_sub }
702 let r_rp: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_RPAREN)
703 if r_rp != 0 { return r_rp }
704 }
705 ci = ci + 1
706 }
707 return 0
708}
709
710// =================================================================
711// Rank-aware pre-scan DFS. Like nx_chem_emit_prescan but:
712// - sorts tree children by Morgan rank ascending before appending
713// - sorts back-edge neighbors by rank ascending before assigning digits
714// - uses an explicit DFS stack with reverse-rank push for canonical
715// traversal order
716//
717// rank[i] = canonical rank of atom i (from nx_chem_morgan_canonical_rank).
718// =================================================================
719func nx_chem_emit_prescan_canonical(m: *MolGraph, s: *EmitState, root: nx_int, rank: *nx_int) -> nx_int {
720 let stack: *nx_int = (sys_mmap((s.n_atoms * 8) as i64)) as *nx_int
721 var stack_n: nx_int = 0
722 stack[stack_n] = root
723 stack_n = stack_n + 1
724 s.visited[root] = 1
725 while stack_n > 0 {
726 stack_n = stack_n - 1
727 let u: nx_int = stack[stack_n]
728 // Collect tree-neighbors and back-edge-neighbors separately
729 let tree_v: *nx_int = (sys_mmap((NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int
730 let tree_o: *nx_int = (sys_mmap((NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int
731 let tree_s: *nx_int = (sys_mmap((NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int
732 var n_tree: nx_int = 0
733 let back_v: *nx_int = (sys_mmap((NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int
734 let back_o: *nx_int = (sys_mmap((NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int
735 var n_back: nx_int = 0
736 var ei: nx_int = 0
737 while ei < m.n_bonds {
738 let b: *Bond = ((m.bonds as nx_int) + (ei * NX_BOND_BYTES)) as *Bond
739 var v: nx_int = -1
740 if b.a == u { v = b.b }
741 if b.b == u { v = b.a }
742 if v >= 0 {
743 if v != s.parent[u] {
744 if s.visited[v] == 1 {
745 if u > v {
746 if n_back < NX_MAX_BACK_PER_ATOM {
747 back_v[n_back] = v
748 back_o[n_back] = b.order
749 n_back = n_back + 1
750 }
751 }
752 }
753 else {
754 // C2.3e: flip bond stereo if DFS direction (u->v) is reversed
755 // relative to bond's stored (a->b) direction.
756 var bond_stereo: nx_int = b.stereo
757 if u == b.b {
758 if bond_stereo == NX_BSTEREO_UP { bond_stereo = NX_BSTEREO_DOWN }
759 else {
760 if bond_stereo == NX_BSTEREO_DOWN { bond_stereo = NX_BSTEREO_UP }
761 }
762 }
763 if n_tree < NX_MAX_TREE_PER_ATOM {
764 tree_v[n_tree] = v
765 tree_o[n_tree] = b.order
766 tree_s[n_tree] = bond_stereo
767 n_tree = n_tree + 1
768 }
769 }
770 }
771 }
772 ei = ei + 1
773 }
774 // Insertion sort tree-neighbors by rank ascending
775 var i_t: nx_int = 1
776 while i_t < n_tree {
777 let kv: nx_int = tree_v[i_t]
778 let ko: nx_int = tree_o[i_t]
779 let ks: nx_int = tree_s[i_t]
780 let kr: nx_int = rank[kv]
781 var j_t: nx_int = i_t - 1
782 var done_t: nx_int = 0
783 while done_t == 0 {
784 if j_t < 0 { done_t = 1 }
785 else {
786 if rank[tree_v[j_t]] <= kr { done_t = 1 }
787 else {
788 tree_v[j_t + 1] = tree_v[j_t]
789 tree_o[j_t + 1] = tree_o[j_t]
790 tree_s[j_t + 1] = tree_s[j_t]
791 j_t = j_t - 1
792 }
793 }
794 }
795 tree_v[j_t + 1] = kv
796 tree_o[j_t + 1] = ko
797 tree_s[j_t + 1] = ks
798 i_t = i_t + 1
799 }
800 // Append tree children in rank-ascending order
801 var k: nx_int = 0
802 while k < n_tree {
803 let ct: nx_int = s.tree_count[u]
804 s.tree_child[(u * NX_MAX_TREE_PER_ATOM) + ct] = tree_v[k]
805 s.tree_bond[(u * NX_MAX_TREE_PER_ATOM) + ct] = tree_o[k]
806 s.tree_stereo[(u * NX_MAX_TREE_PER_ATOM) + ct] = tree_s[k]
807 s.tree_count[u] = ct + 1
808 s.visited[tree_v[k]] = 1
809 s.parent[tree_v[k]] = u
810 k = k + 1
811 }
812 // Push tree-neighbors onto stack in REVERSE rank order so the
813 // lowest-rank child is popped first (becomes the next emit
814 // target after u; canonical traversal walks the lowest-rank
815 // branch first).
816 var k2: nx_int = n_tree - 1
817 while k2 >= 0 {
818 stack[stack_n] = tree_v[k2]
819 stack_n = stack_n + 1
820 k2 = k2 - 1
821 }
822 // Sort back-edge neighbors by rank ascending
823 var i_b: nx_int = 1
824 while i_b < n_back {
825 let kv2: nx_int = back_v[i_b]
826 let ko2: nx_int = back_o[i_b]
827 let kr2: nx_int = rank[kv2]
828 var j_b: nx_int = i_b - 1
829 var done_b: nx_int = 0
830 while done_b == 0 {
831 if j_b < 0 { done_b = 1 }
832 else {
833 if rank[back_v[j_b]] <= kr2 { done_b = 1 }
834 else {
835 back_v[j_b + 1] = back_v[j_b]
836 back_o[j_b + 1] = back_o[j_b]
837 j_b = j_b - 1
838 }
839 }
840 }
841 back_v[j_b + 1] = kv2
842 back_o[j_b + 1] = ko2
843 i_b = i_b + 1
844 }
845 // Allocate ring digits in rank-sorted order
846 var kb: nx_int = 0
847 while kb < n_back {
848 let v2: nx_int = back_v[kb]
849 let bo: nx_int = back_o[kb]
850 let d: nx_int = s.next_digit
851 s.next_digit = s.next_digit + 1
852 let cu: nx_int = s.back_count[u]
853 if cu < NX_MAX_BACK_PER_ATOM {
854 s.back_digit[(u * NX_MAX_BACK_PER_ATOM) + cu] = d
855 s.back_bond[(u * NX_MAX_BACK_PER_ATOM) + cu] = bo
856 s.back_count[u] = cu + 1
857 }
858 let cv: nx_int = s.back_count[v2]
859 if cv < NX_MAX_BACK_PER_ATOM {
860 s.back_digit[(v2 * NX_MAX_BACK_PER_ATOM) + cv] = d
861 s.back_bond[(v2 * NX_MAX_BACK_PER_ATOM) + cv] = bo
862 s.back_count[v2] = cv + 1
863 }
864 kb = kb + 1
865 }
866 }
867 return 0
868}
869
870// =================================================================
871// Canonical SMILES emit. Computes Morgan rank, then walks each
872// connected component starting from the lowest-rank unvisited atom,
873// ordering children by rank. Output is byte-identical for two
874// MolGraphs representing the same molecule.
875// =================================================================
876func nx_chem_emit_canonical_smiles(m: *MolGraph, out_buf: *u8, cap: nx_int, out_len_io: *nx_int) -> nx_int {
877 out_len_io[0] = 0
878 if m.n_atoms == 0 { return 0 }
879 let rank: *nx_int = (sys_mmap((m.n_atoms * 8) as i64)) as *nx_int
880 let _r: nx_int = nx_chem_morgan_canonical_rank(m, rank)
881 let s: *EmitState = nx_chem_emit_state_new(m.n_atoms)
882 var first_component: nx_int = 1
883 var more: nx_int = 1
884 while more == 1 {
885 // find unvisited atom with lowest rank
886 var root: nx_int = -1
887 var best_rank: nx_int = m.n_atoms + 1
888 var ri: nx_int = 0
889 while ri < m.n_atoms {
890 if s.visited[ri] == 0 {
891 if rank[ri] < best_rank {
892 best_rank = rank[ri]
893 root = ri
894 }
895 }
896 ri = ri + 1
897 }
898 if root < 0 { more = 0 }
899 else {
900 if first_component == 0 {
901 let r_dot: nx_int = nx_chem_emit_byte(out_buf, out_len_io, cap, NX_C_DOT)
902 if r_dot != 0 { return r_dot }
903 }
904 first_component = 0
905 let _p: nx_int = nx_chem_emit_prescan_canonical(m, s, root, rank)
906 let r_sub: nx_int = nx_chem_emit_subtree(out_buf, out_len_io, cap, m, s, root, NX_BOND_SINGLE, NX_BSTEREO_NONE, 0)
907 if r_sub != 0 { return r_sub }
908 }
909 }
910 return 0
911}
912
913// =================================================================
914// Main entry: MolGraph -> SMILES. Writes into out_buf (capacity
915// cap); out_len_io receives the byte count written. Returns 0 on
916// success or -1 on overflow.
917//
918// Multi-component graphs: each connected component is emitted in
919// turn, separated by '.'.
920// =================================================================
921func nx_chem_emit_smiles(m: *MolGraph, out_buf: *u8, cap: nx_int, out_len_io: *nx_int) -> nx_int {
922 out_len_io[0] = 0
923 if m.n_atoms == 0 { return 0 }
924 let s: *EmitState = nx_chem_emit_state_new(m.n_atoms)
925 var first_component: nx_int = 1
926 var root: nx_int = 0
927 while root < m.n_atoms {
928 if s.visited[root] == 0 {
929 // emit '.' separator between components
930 if first_component == 0 {
931 let r_dot: nx_int = nx_chem_emit_byte(out_buf, out_len_io, cap, NX_C_DOT)
932 if r_dot != 0 { return r_dot }
933 }
934 first_component = 0
935 // pre-scan from this root
936 let _p: nx_int = nx_chem_emit_prescan(m, s, root)
937 // emit the subtree rooted at root (bond_in = NX_BOND_SINGLE is harmless
938 // since the bond_marker emit omits single bonds anyway)
939 let r_sub: nx_int = nx_chem_emit_subtree(out_buf, out_len_io, cap, m, s, root, NX_BOND_SINGLE, NX_BSTEREO_NONE, 0)
940 if r_sub != 0 { return r_sub }
941 }
942 root = root + 1
943 }
944 return 0
945}