code wiki / (root) / nx_vcf.nx

nx_vcf.nx source

↩ module page · 436 lines · 20836 B

1// nx_vcf.nx -- VCF (Variant Call Format) single-line writer. 2// 3// license_tier: INDEPENDENT_REDERIVE 4// genealogy_id: international-research-sources/danecek-2011-vcf-spec-v4.2 5// 6// G2.4 of NISHI_GENOMICS_SUBSTRATE_ROADMAP.md. Closes the variant 7// output side: called variants from snv_call become VCF lines that 8// every downstream tool (bcftools / vcftools / IGV / ClinVar / 9// dbSNP / gnomAD / nf-core pipelines) consumes. 10// 11// VCF v4.2 data-line format (Danecek 2011, NCBI hosts canonical): 12// CHROM POS ID REF ALT QUAL FILTER INFO [FORMAT SAMPLE...] 13// tab-separated, terminated by LF (or CRLF on Windows) 14// 15// G2.4 emits the 8 required fields: 16// CHROM : caller-supplied reference sequence name (no validation) 17// POS : 1-indexed (VCF spec); caller converts from substrate's 18// 0-indexed internal coords 19// ID : "." (no rsID lookup yet; G2.4b composes dbSNP table) 20// REF : single ACGT letter from ref_code (NX_DNA_A/C/G/T) 21// ALT : single ACGT letter from alt_code 22// QUAL : integer Phred-scaled quality (typically 0..60 mapq-equivalent) 23// FILTER : "PASS" if pass_flag=1, "." if 0 24// INFO : "DP=<depth>" (caller can extend; G2.4 just emits DP) 25// 26// Why a single-line writer (not a streaming serialiser): 27// VCF lines are line-by-line composable. Caller drives the per- 28// variant loop, calls this per variant, accumulates bytes into a 29// buffer or writes them to a file. Keeps this primitive small + 30// composable + testable without I/O surface. 31// 32// What G2.4 does NOT do (deferred): 33// - VCF header (##fileformat=VCFv4.2 + ##INFO + ##FILTER + #CHROM) -- G2.4a 34// - Multi-allelic ALT (A>G,T) -- G2.4b 35// - FORMAT + per-sample fields (GT:AD:DP) -- G2.4c 36// - INFO extension (AF, AC, AN, MQ, MQRankSum, etc.) -- G2.4d 37// - VCF parsing (read existing VCF files) -- G2.4e 38// - bgzip + tabix indexing -- G2.4f 39// - VCF -> SAM rsID annotation lookup -- G2.4g 40// 41// API: 42// vcf_write_snv_line( 43// chrom, chrom_len, 44// fields, // 6 i64s: [pos, ref, alt, qual, pass_flag, depth] 45// out_ascii, max_out 46// ) -> i64 bytes written or -1 47// 48// Field bundling works around the nxc2 codegen issue with 9+ scalar 49// arguments; same trick as nx_align_score's region/sw_end bundles. 50// 51// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 52// intended_use: "VCF v4.2 single-line variant output; 53// the format every downstream variant 54// consumer (bcftools, IGV, ClinVar, etc.) 55// ingests" 56// sil_target: SIL2 57// asil_target: QM 58// dal_target: DAL C 59// iec_62304_class: B 60// evidence: [no_floating_point, deterministic, 61// bit_equal_reproducible, 62// danecek_2011_vcf_v4_2_spec, 63// composes_nx_cigar_i64_to_ascii_digits, 64// composes_nx_const_NX_DNA_to_ascii, 65// canonical_chr1_12345_A_G_KAT, 66// license_tier_INDEPENDENT_REDERIVE] 67// hazard_register: [bug-tape-vcf-pos-0-indexed-leaked-to-output, 68// bug-tape-vcf-tab-vs-space-separator-confused, 69// bug-tape-vcf-ref-alt-same-code-emitted-as-variant, 70// bug-tape-vcf-line-not-lf-terminated, 71// bug-tape-vcf-bytes-written-mismatched-actual] 72// residual_risk: "G2.4 does not validate ref_code != alt_code 73// because a variant where they match is the 74// snv_call NONE case which the caller should 75// not be writing as a VCF line. Documented 76// in header; caller-side guard." 77// verdict: NOT_YET_EVALUATED 78 79import "nx_syscalls.nx" 80import "nx_const.nx" 81import "nx_sequence.nx" // for dna_decode_base 82import "nx_cigar.nx" // for nx_i64_to_ascii_digits 83 84// Internal: copy a byte string into out_ascii. Returns bytes copied. 85// Returns -1 if max_out exceeded (parameter is the REMAINING capacity). 86func nx_copy_bytes(src: *u8, src_len: i64, 87 dst: *u8, dst_off: i64, max_dst: i64) -> i64 { 88 if dst_off + src_len > max_dst { return -1 } 89 var i: i64 = 0 90 while i < src_len { 91 dst[dst_off + i] = src[i] & 0xff 92 i = i + 1 93 } 94 return src_len 95} 96 97// Internal: copy a one-byte literal. 98func nx_copy_byte(b: i64, dst: *u8, dst_off: i64, max_dst: i64) -> i64 { 99 if dst_off >= max_dst { return -1 } 100 dst[dst_off] = b & 0xff 101 return 1 102} 103 104// Internal: emit decimal integer + return bytes written. 105func nx_emit_int(n: i64, dst: *u8, dst_off: i64, max_dst: i64) -> i64 { 106 let scratch: *u8 = sys_mmap(32) 107 let d_count: i64 = nx_i64_to_ascii_digits(n, scratch) 108 if dst_off + d_count > max_dst { return -1 } 109 var i: i64 = 0 110 while i < d_count { 111 dst[dst_off + i] = scratch[i] & 0xff 112 i = i + 1 113 } 114 return d_count 115} 116 117// Field indices into the bundled fields[6] array for SNV lines. 118const NX_VCF_FIELD_POS: i64 = 0 119const NX_VCF_FIELD_REF: i64 = 1 120const NX_VCF_FIELD_ALT: i64 = 2 121const NX_VCF_FIELD_QUAL: i64 = 3 122const NX_VCF_FIELD_PASS: i64 = 4 123const NX_VCF_FIELD_DEPTH: i64 = 5 124 125// Field indices for multi-byte-allele variant lines (indels / MNVs). 126// REF and ALT bases are passed via ref_alt byte array (concat'd); 127// lengths live in the fields[] array. 128const NX_VCF_VAR_FIELD_POS: i64 = 0 129const NX_VCF_VAR_FIELD_QUAL: i64 = 1 130const NX_VCF_VAR_FIELD_PASS: i64 = 2 131const NX_VCF_VAR_FIELD_DEPTH: i64 = 3 132const NX_VCF_VAR_FIELD_REF_LEN: i64 = 4 133const NX_VCF_VAR_FIELD_ALT_LEN: i64 = 5 134 135// Emit the minimal VCFv4.2 header into out_ascii. Four lines: 136// ##fileformat=VCFv4.2 137// ##INFO=<ID=DP,Number=1,Type=Integer,Description="Total Depth"> 138// ##FILTER=<ID=PASS,Description="All filters passed"> 139// #CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO 140// 141// Returns total bytes written or -1 if max_out insufficient. 142// The header is fixed (no parameters): callers that need to extend 143// INFO / FILTER definitions append additional ## lines themselves 144// before emitting the first data line via vcf_write_snv_line. 145// 146// Byte total = 21 + 63 + 52 + 39 = 175 (each line LF-terminated). 147func vcf_write_header(out_ascii: *u8, max_out: i64) -> i64 { 148 let total: i64 = 175 149 if max_out < total { return -1 } 150 151 var off: i64 = 0 152 153 // ---- Line 1: "##fileformat=VCFv4.2\n" (21 bytes) ---- 154 out_ascii[off+0]=0x23; out_ascii[off+1]=0x23 // ## 155 out_ascii[off+2]=0x66; out_ascii[off+3]=0x69; out_ascii[off+4]=0x6C; out_ascii[off+5]=0x65 // file 156 out_ascii[off+6]=0x66; out_ascii[off+7]=0x6F; out_ascii[off+8]=0x72; out_ascii[off+9]=0x6D; out_ascii[off+10]=0x61; out_ascii[off+11]=0x74 // format 157 out_ascii[off+12]=0x3D // = 158 out_ascii[off+13]=0x56; out_ascii[off+14]=0x43; out_ascii[off+15]=0x46 // VCF 159 out_ascii[off+16]=0x76; out_ascii[off+17]=0x34; out_ascii[off+18]=0x2E; out_ascii[off+19]=0x32 // v4.2 160 out_ascii[off+20]=0x0A // \n 161 off = off + 21 162 163 // ---- Line 2: "##INFO=<ID=DP,Number=1,Type=Integer,Description=\"Total Depth\">\n" (62 bytes) ---- 164 out_ascii[off+0]=0x23; out_ascii[off+1]=0x23 // ## 165 out_ascii[off+2]=0x49; out_ascii[off+3]=0x4E; out_ascii[off+4]=0x46; out_ascii[off+5]=0x4F // INFO 166 out_ascii[off+6]=0x3D; out_ascii[off+7]=0x3C // =< 167 out_ascii[off+8]=0x49; out_ascii[off+9]=0x44; out_ascii[off+10]=0x3D; out_ascii[off+11]=0x44; out_ascii[off+12]=0x50 // ID=DP 168 out_ascii[off+13]=0x2C // , 169 out_ascii[off+14]=0x4E; out_ascii[off+15]=0x75; out_ascii[off+16]=0x6D; out_ascii[off+17]=0x62; out_ascii[off+18]=0x65; out_ascii[off+19]=0x72 // Number 170 out_ascii[off+20]=0x3D; out_ascii[off+21]=0x31 // =1 171 out_ascii[off+22]=0x2C // , 172 out_ascii[off+23]=0x54; out_ascii[off+24]=0x79; out_ascii[off+25]=0x70; out_ascii[off+26]=0x65 // Type 173 out_ascii[off+27]=0x3D // = 174 out_ascii[off+28]=0x49; out_ascii[off+29]=0x6E; out_ascii[off+30]=0x74; out_ascii[off+31]=0x65; out_ascii[off+32]=0x67; out_ascii[off+33]=0x65; out_ascii[off+34]=0x72 // Integer 175 out_ascii[off+35]=0x2C // , 176 out_ascii[off+36]=0x44; out_ascii[off+37]=0x65; out_ascii[off+38]=0x73; out_ascii[off+39]=0x63; out_ascii[off+40]=0x72; out_ascii[off+41]=0x69; out_ascii[off+42]=0x70; out_ascii[off+43]=0x74; out_ascii[off+44]=0x69; out_ascii[off+45]=0x6F; out_ascii[off+46]=0x6E // Description 177 out_ascii[off+47]=0x3D; out_ascii[off+48]=0x22 // =" 178 out_ascii[off+49]=0x54; out_ascii[off+50]=0x6F; out_ascii[off+51]=0x74; out_ascii[off+52]=0x61; out_ascii[off+53]=0x6C // Total 179 out_ascii[off+54]=0x20 // space 180 out_ascii[off+55]=0x44; out_ascii[off+56]=0x65; out_ascii[off+57]=0x70; out_ascii[off+58]=0x74; out_ascii[off+59]=0x68 // Depth 181 out_ascii[off+60]=0x22; out_ascii[off+61]=0x3E // "> 182 // Wait, that's 62 bytes (0..61) but I need 62 total including LF. 183 // Let me recount: I've assigned 0..61 = 62 positions = 62 bytes including '>'. 184 // Need to add \n. So actually 63 bytes total for line 2 incl LF. Fixing: 185 out_ascii[off+62]=0x0A // \n 186 off = off + 63 // line 2 is actually 63 bytes incl LF 187 188 // ---- Line 3: "##FILTER=<ID=PASS,Description=\"All filters passed\">\n" (52 bytes) ---- 189 out_ascii[off+0]=0x23; out_ascii[off+1]=0x23 // ## 190 out_ascii[off+2]=0x46; out_ascii[off+3]=0x49; out_ascii[off+4]=0x4C; out_ascii[off+5]=0x54; out_ascii[off+6]=0x45; out_ascii[off+7]=0x52 // FILTER 191 out_ascii[off+8]=0x3D; out_ascii[off+9]=0x3C // =< 192 out_ascii[off+10]=0x49; out_ascii[off+11]=0x44; out_ascii[off+12]=0x3D; out_ascii[off+13]=0x50; out_ascii[off+14]=0x41; out_ascii[off+15]=0x53; out_ascii[off+16]=0x53 // ID=PASS 193 out_ascii[off+17]=0x2C // , 194 out_ascii[off+18]=0x44; out_ascii[off+19]=0x65; out_ascii[off+20]=0x73; out_ascii[off+21]=0x63; out_ascii[off+22]=0x72; out_ascii[off+23]=0x69; out_ascii[off+24]=0x70; out_ascii[off+25]=0x74; out_ascii[off+26]=0x69; out_ascii[off+27]=0x6F; out_ascii[off+28]=0x6E // Description 195 out_ascii[off+29]=0x3D; out_ascii[off+30]=0x22 // =" 196 out_ascii[off+31]=0x41; out_ascii[off+32]=0x6C; out_ascii[off+33]=0x6C // All 197 out_ascii[off+34]=0x20 // space 198 out_ascii[off+35]=0x66; out_ascii[off+36]=0x69; out_ascii[off+37]=0x6C; out_ascii[off+38]=0x74; out_ascii[off+39]=0x65; out_ascii[off+40]=0x72; out_ascii[off+41]=0x73 // filters 199 out_ascii[off+42]=0x20 // space 200 out_ascii[off+43]=0x70; out_ascii[off+44]=0x61; out_ascii[off+45]=0x73; out_ascii[off+46]=0x73; out_ascii[off+47]=0x65; out_ascii[off+48]=0x64 // passed 201 out_ascii[off+49]=0x22; out_ascii[off+50]=0x3E // "> 202 out_ascii[off+51]=0x0A // \n 203 off = off + 52 204 205 // ---- Line 4: "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n" (38 bytes) ---- 206 out_ascii[off+0]=0x23 // # 207 out_ascii[off+1]=0x43; out_ascii[off+2]=0x48; out_ascii[off+3]=0x52; out_ascii[off+4]=0x4F; out_ascii[off+5]=0x4D // CHROM 208 out_ascii[off+6]=0x09 // \t 209 out_ascii[off+7]=0x50; out_ascii[off+8]=0x4F; out_ascii[off+9]=0x53 // POS 210 out_ascii[off+10]=0x09 // \t 211 out_ascii[off+11]=0x49; out_ascii[off+12]=0x44 // ID 212 out_ascii[off+13]=0x09 // \t 213 out_ascii[off+14]=0x52; out_ascii[off+15]=0x45; out_ascii[off+16]=0x46 // REF 214 out_ascii[off+17]=0x09 // \t 215 out_ascii[off+18]=0x41; out_ascii[off+19]=0x4C; out_ascii[off+20]=0x54 // ALT 216 out_ascii[off+21]=0x09 // \t 217 out_ascii[off+22]=0x51; out_ascii[off+23]=0x55; out_ascii[off+24]=0x41; out_ascii[off+25]=0x4C // QUAL 218 out_ascii[off+26]=0x09 // \t 219 out_ascii[off+27]=0x46; out_ascii[off+28]=0x49; out_ascii[off+29]=0x4C; out_ascii[off+30]=0x54; out_ascii[off+31]=0x45; out_ascii[off+32]=0x52 // FILTER 220 out_ascii[off+33]=0x09 // \t 221 out_ascii[off+34]=0x49; out_ascii[off+35]=0x4E; out_ascii[off+36]=0x46; out_ascii[off+37]=0x4F // INFO 222 out_ascii[off+38]=0x0A // \n 223 off = off + 39 // 39 bytes incl LF, not 38 224 225 return off 226} 227 228// Emit one VCF data line with MULTI-BYTE REF and ALT alleles 229// (indels / MNVs / arbitrary substitutions). Per VCF v4.2 spec: 230// Insertion : REF = base before insertion site (1 byte) 231// ALT = REF + inserted bases 232// Deletion : REF = base before deletion + deleted bases 233// ALT = base before deletion (1 byte) 234// General : any non-empty REF / ALT strings (both required >=1 byte) 235// 236// API: 237// vcf_write_var_line( 238// chrom, chrom_len, 239// fields, // [pos, qual, pass_flag, depth, ref_len, alt_len] 240// ref_alt, // REF bases concat'd with ALT bases 241// // (length = ref_len + alt_len; REF is first) 242// out_ascii, max_out 243// ) -> i64 bytes written or -1 244// 245// Args bundled per recurring nxc2 codegen quirk (~10 scalar args 246// silently returns -1; bundle inputs into i64 array + ref_alt byte 247// stream). 248func vcf_write_var_line(chrom: *u8, chrom_len: i64, 249 fields: *i64, 250 ref_alt: *u8, 251 out_ascii: *u8, max_out: i64) -> i64 { 252 if chrom_len < 0 { return -1 } 253 let pos_1indexed: i64 = fields[NX_VCF_VAR_FIELD_POS] 254 let qual: i64 = fields[NX_VCF_VAR_FIELD_QUAL] 255 let pass_flag: i64 = fields[NX_VCF_VAR_FIELD_PASS] 256 let depth: i64 = fields[NX_VCF_VAR_FIELD_DEPTH] 257 let ref_len: i64 = fields[NX_VCF_VAR_FIELD_REF_LEN] 258 let alt_len: i64 = fields[NX_VCF_VAR_FIELD_ALT_LEN] 259 if pos_1indexed < 1 { return -1 } 260 if qual < 0 { return -1 } 261 if depth < 0 { return -1 } 262 if ref_len < 1 { return -1 } 263 if alt_len < 1 { return -1 } 264 if max_out <= 0 { return -1 } 265 266 var off: i64 = 0 267 268 // ---- CHROM ---- 269 let n_chrom: i64 = nx_copy_bytes(chrom, chrom_len, out_ascii, off, max_out) 270 if n_chrom < 0 { return -1 } 271 off = off + n_chrom 272 273 // ---- tab + POS ---- 274 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 275 off = off + 1 276 let n_pos: i64 = nx_emit_int(pos_1indexed, out_ascii, off, max_out) 277 if n_pos < 0 { return -1 } 278 off = off + n_pos 279 280 // ---- tab + ID ('.') ---- 281 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 282 off = off + 1 283 if nx_copy_byte(NX_ASCII_DOT, out_ascii, off, max_out) < 0 { return -1 } 284 off = off + 1 285 286 // ---- tab + REF (multi-byte) ---- 287 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 288 off = off + 1 289 let n_ref: i64 = nx_copy_bytes(ref_alt, ref_len, out_ascii, off, max_out) 290 if n_ref < 0 { return -1 } 291 off = off + n_ref 292 293 // ---- tab + ALT (multi-byte, starts at ref_alt + ref_len) ---- 294 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 295 off = off + 1 296 let alt_start: *u8 = ((ref_alt as i64) + ref_len) as *u8 297 let n_alt: i64 = nx_copy_bytes(alt_start, alt_len, out_ascii, off, max_out) 298 if n_alt < 0 { return -1 } 299 off = off + n_alt 300 301 // ---- tab + QUAL ---- 302 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 303 off = off + 1 304 let n_qual: i64 = nx_emit_int(qual, out_ascii, off, max_out) 305 if n_qual < 0 { return -1 } 306 off = off + n_qual 307 308 // ---- tab + FILTER ---- 309 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 310 off = off + 1 311 if pass_flag == 1 { 312 if off + 4 > max_out { return -1 } 313 out_ascii[off] = 0x50 314 out_ascii[off+1] = 0x41 315 out_ascii[off+2] = 0x53 316 out_ascii[off+3] = 0x53 317 off = off + 4 318 } else { 319 if nx_copy_byte(NX_ASCII_DOT, out_ascii, off, max_out) < 0 { return -1 } 320 off = off + 1 321 } 322 323 // ---- tab + INFO "DP=<depth>" ---- 324 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 325 off = off + 1 326 if off + 3 > max_out { return -1 } 327 out_ascii[off] = 0x44 328 out_ascii[off+1] = 0x50 329 out_ascii[off+2] = 0x3D 330 off = off + 3 331 let n_dp: i64 = nx_emit_int(depth, out_ascii, off, max_out) 332 if n_dp < 0 { return -1 } 333 off = off + n_dp 334 335 // ---- terminating LF ---- 336 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 337 off = off + 1 338 339 return off 340} 341 342// Emit one VCF SNV data line per spec. See header for API. 343func vcf_write_snv_line(chrom: *u8, chrom_len: i64, 344 fields: *i64, 345 out_ascii: *u8, max_out: i64) -> i64 { 346 if chrom_len < 0 { return -1 } 347 let pos_1indexed: i64 = fields[NX_VCF_FIELD_POS] 348 let ref_code: i64 = fields[NX_VCF_FIELD_REF] 349 let alt_code: i64 = fields[NX_VCF_FIELD_ALT] 350 let qual: i64 = fields[NX_VCF_FIELD_QUAL] 351 let pass_flag: i64 = fields[NX_VCF_FIELD_PASS] 352 let depth: i64 = fields[NX_VCF_FIELD_DEPTH] 353 if pos_1indexed < 1 { return -1 } 354 if ref_code < 0 { return -1 } 355 if ref_code > 3 { return -1 } 356 if alt_code < 0 { return -1 } 357 if alt_code > 3 { return -1 } 358 if qual < 0 { return -1 } 359 if depth < 0 { return -1 } 360 if max_out <= 0 { return -1 } 361 362 var off: i64 = 0 363 364 // ---- CHROM ---- 365 let n_chrom: i64 = nx_copy_bytes(chrom, chrom_len, out_ascii, off, max_out) 366 if n_chrom < 0 { return -1 } 367 off = off + n_chrom 368 369 // ---- tab + POS ---- 370 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 371 off = off + 1 372 let n_pos: i64 = nx_emit_int(pos_1indexed, out_ascii, off, max_out) 373 if n_pos < 0 { return -1 } 374 off = off + n_pos 375 376 // ---- tab + ID ('.') ---- 377 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 378 off = off + 1 379 if nx_copy_byte(NX_ASCII_DOT, out_ascii, off, max_out) < 0 { return -1 } 380 off = off + 1 381 382 // ---- tab + REF letter ---- 383 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 384 off = off + 1 385 let ref_letter: i64 = dna_decode_base(ref_code) 386 if nx_copy_byte(ref_letter, out_ascii, off, max_out) < 0 { return -1 } 387 off = off + 1 388 389 // ---- tab + ALT letter ---- 390 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 391 off = off + 1 392 let alt_letter: i64 = dna_decode_base(alt_code) 393 if nx_copy_byte(alt_letter, out_ascii, off, max_out) < 0 { return -1 } 394 off = off + 1 395 396 // ---- tab + QUAL ---- 397 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 398 off = off + 1 399 let n_qual: i64 = nx_emit_int(qual, out_ascii, off, max_out) 400 if n_qual < 0 { return -1 } 401 off = off + n_qual 402 403 // ---- tab + FILTER ---- 404 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 405 off = off + 1 406 if pass_flag == 1 { 407 // "PASS" = 4 bytes 0x50 0x41 0x53 0x53 408 if off + 4 > max_out { return -1 } 409 out_ascii[off] = 0x50 // P 410 out_ascii[off+1] = 0x41 // A 411 out_ascii[off+2] = 0x53 // S 412 out_ascii[off+3] = 0x53 // S 413 off = off + 4 414 } else { 415 if nx_copy_byte(NX_ASCII_DOT, out_ascii, off, max_out) < 0 { return -1 } 416 off = off + 1 417 } 418 419 // ---- tab + INFO "DP=<depth>" ---- 420 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 421 off = off + 1 422 if off + 3 > max_out { return -1 } 423 out_ascii[off] = 0x44 // D 424 out_ascii[off+1] = 0x50 // P 425 out_ascii[off+2] = 0x3D // = 426 off = off + 3 427 let n_dp: i64 = nx_emit_int(depth, out_ascii, off, max_out) 428 if n_dp < 0 { return -1 } 429 off = off + n_dp 430 431 // ---- terminating LF ---- 432 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 433 off = off + 1 434 435 return off 436}