code wiki / (root) / nx_sam.nx

nx_sam.nx source

↩ module page · 669 lines · 25517 B

1// nx_sam.nx -- SAM (Sequence Alignment / Map) single-line writer. 2// 3// license_tier: INDEPENDENT_REDERIVE 4// genealogy_id: international-research-sources/li-2009-sam-spec-v1.4.6 5// 6// G6.0 of NISHI_GENOMICS_SUBSTRATE_ROADMAP.md. Closes the aligned- 7// read output side, parallel to the VCF variant output already 8// shipped in nx_vcf.nx. SAM is the universal interchange format 9// for aligned sequencer reads -- consumed by every downstream 10// tool (bcftools, samtools, IGV, GATK, snakemake pipelines). 11// 12// SAM v1.4.6 data-line format (Li 2009 + SAMv1 spec): 13// QNAME FLAG RNAME POS MAPQ CIGAR RNEXT PNEXT TLEN SEQ QUAL [tags] 14// tab-separated, terminated by LF (or CRLF on Windows) 15// 16// G6.0 emits the 11 mandatory fields: 17// QNAME : caller-supplied read identifier 18// FLAG : SAM flag bitfield (0 for unpaired + mapped + fwd strand) 19// RNAME : reference sequence name (chrom) 20// POS : 1-indexed leftmost mapping position 21// MAPQ : mapping quality 0..60 (from nx_alignment.AlignmentRecord) 22// CIGAR : ASCII CIGAR string (from nx_cigar.cigar_serialize) 23// RNEXT : "*" hard-coded (unpaired); paired-end is G6.0b 24// PNEXT : 0 hard-coded (unpaired) 25// TLEN : caller-supplied template length (0 for single read) 26// SEQ : query bases as ASCII (caller decodes from packed via dna_unpack) 27// QUAL : Phred+33 quality string (caller encodes via nx_phred33_encode_string) 28// 29// Bundled-args API (5 total) per the recurring nxc2 codegen quirk 30// where ~10 scalar args silently fail. Strings concatenated into 31// one buffer + per-string lengths array; numeric fields in a second 32// array. 33// 34// API: 35// sam_write_line(str_buf, str_lens, fields, out_ascii, max_out) -> i64 36// 37// str_buf : qname || rname || cigar || seq || qual (concat) 38// str_lens : [qname_len, rname_len, cigar_len, seq_len] 39// qual_len == seq_len per SAM-spec invariant 40// fields : [flag, pos_1indexed, mapq, tlen] 41// out_ascii : output buffer 42// max_out : capacity 43// Returns bytes written or -1 on bad input / capacity overrun. 44// 45// What G6.0 does NOT do (deferred): 46// - SAM header writer (@HD + @SQ + @RG + @PG lines) -- G6.0a 47// - Paired-end mate fields (RNEXT/PNEXT/TLEN from mate) -- G6.0b 48// - Optional TAG fields (RG:Z, NM:i, MD:Z, AS:i, etc.) -- G6.0c 49// - BAM binary serialiser (uint32-packed records) -- G6.0d 50// - SAM parsing (read existing SAM files) -- G6.0e 51// - bgzip + .bai indexing -- G6.0f 52// 53// Indices into the bundled fields array. 54// NX_SAM_FIELD_FLAG = 0 55// NX_SAM_FIELD_POS = 1 (1-indexed leftmost mapping position) 56// NX_SAM_FIELD_MAPQ = 2 57// NX_SAM_FIELD_TLEN = 3 (signed; SAM spec allows negative) 58// Indices into the bundled str_lens array. 59// NX_SAM_STR_QNAME = 0 60// NX_SAM_STR_RNAME = 1 61// NX_SAM_STR_CIGAR = 2 62// NX_SAM_STR_SEQ = 3 (qual length identical; not stored separately) 63// 64// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 65// intended_use: "SAM v1.4.6 single-line aligned-read output; 66// the universal aligned-read interchange the 67// downstream variant-calling + analysis 68// ecosystem consumes" 69// sil_target: SIL2 70// asil_target: QM 71// dal_target: DAL C 72// iec_62304_class: B 73// evidence: [no_floating_point, deterministic, 74// bit_equal_reproducible, 75// li_2009_sam_spec_v1_4_6, 76// composes_nx_vcf_helpers_nx_emit_int_nx_copy_bytes, 77// canonical_unpaired_mapped_KAT, 78// bundled_args_avoids_nxc2_codegen_quirk, 79// license_tier_INDEPENDENT_REDERIVE] 80// hazard_register: [bug-tape-sam-pos-0-indexed-leaked, 81// bug-tape-sam-seq-qual-length-mismatch-not-validated, 82// bug-tape-sam-flag-bitfield-misset, 83// bug-tape-sam-line-cr-lf-vs-lf-line-ending] 84// residual_risk: "G6.0 hard-codes RNEXT=* and PNEXT=0 for 85// unpaired reads. Paired-end is G6.0b. 86// TLEN is signed in SAM spec; this primitive 87// emits as-is (caller responsible for sign)." 88// verdict: NOT_YET_EVALUATED 89 90import "nx_syscalls.nx" 91import "nx_const.nx" 92import "nx_sequence.nx" // (not strictly needed but ecosystem composition) 93import "nx_cigar.nx" // for nx_i64_to_ascii_digits 94import "nx_vcf.nx" // for nx_copy_bytes / nx_copy_byte / nx_emit_int helpers 95 96const NX_SAM_FIELD_FLAG: i64 = 0 97const NX_SAM_FIELD_POS: i64 = 1 98const NX_SAM_FIELD_MAPQ: i64 = 2 99const NX_SAM_FIELD_TLEN: i64 = 3 100 101const NX_SAM_STR_QNAME: i64 = 0 102const NX_SAM_STR_RNAME: i64 = 1 103const NX_SAM_STR_CIGAR: i64 = 2 104const NX_SAM_STR_SEQ: i64 = 3 105 106// G6.0b paired-end field indices. 107// fields = [flag, pos, mapq, pnext, tlen] 108// str_lens = [qname_len, rname_len, rnext_len, cigar_len, seq_len] 109// str_buf = qname || rname || rnext || cigar || seq || qual 110// 111// RNEXT convention: 112// "=" - mate aligns to the same reference (RNAME) 113// "*" - mate is unmapped 114// <name> - mate aligns to a different reference (chrom name) 115const NX_SAM_PE_FIELD_FLAG: i64 = 0 116const NX_SAM_PE_FIELD_POS: i64 = 1 117const NX_SAM_PE_FIELD_MAPQ: i64 = 2 118const NX_SAM_PE_FIELD_PNEXT: i64 = 3 119const NX_SAM_PE_FIELD_TLEN: i64 = 4 120 121const NX_SAM_PE_STR_QNAME: i64 = 0 122const NX_SAM_PE_STR_RNAME: i64 = 1 123const NX_SAM_PE_STR_RNEXT: i64 = 2 124const NX_SAM_PE_STR_CIGAR: i64 = 3 125const NX_SAM_PE_STR_SEQ: i64 = 4 126 127// Emit the SAM v1.6 @HD header line: 128// "@HD\tVN:1.6\tSO:coordinate\n" (25 bytes) 129// Fixed content for G6.0a; downstream extension can add GO:none + SS: 130// tags via a separate primitive (G6.0a.2). 131func sam_write_hd_line(out_ascii: *u8, max_out: i64) -> i64 { 132 let total: i64 = 25 133 if max_out < total { return -1 } 134 var off: i64 = 0 135 // @HD 136 out_ascii[off+0]=0x40; out_ascii[off+1]=0x48; out_ascii[off+2]=0x44 137 out_ascii[off+3]=0x09 // \t 138 // VN:1.6 139 out_ascii[off+4]=0x56; out_ascii[off+5]=0x4E; out_ascii[off+6]=0x3A // VN: 140 out_ascii[off+7]=0x31; out_ascii[off+8]=0x2E; out_ascii[off+9]=0x36 // 1.6 141 out_ascii[off+10]=0x09 // \t 142 // SO:coordinate 143 out_ascii[off+11]=0x53; out_ascii[off+12]=0x4F; out_ascii[off+13]=0x3A // SO: 144 out_ascii[off+14]=0x63; out_ascii[off+15]=0x6F; out_ascii[off+16]=0x6F // coo 145 out_ascii[off+17]=0x72; out_ascii[off+18]=0x64; out_ascii[off+19]=0x69 // rdi 146 out_ascii[off+20]=0x6E; out_ascii[off+21]=0x61; out_ascii[off+22]=0x74 // nat 147 out_ascii[off+23]=0x65 // e 148 out_ascii[off+24]=0x0A // \n 149 off = off + 25 150 return off 151} 152 153// Index constants for sam_write_pg_line's str_lens array. 154const NX_SAM_PG_ID: i64 = 0 155const NX_SAM_PG_NAME: i64 = 1 156const NX_SAM_PG_VERSION: i64 = 2 157 158// Index constants for sam_write_rg_line's str_lens array. 159const NX_SAM_RG_ID: i64 = 0 160const NX_SAM_RG_SAMPLE: i64 = 1 161 162// Emit one SAM @RG read-group header line (minimum: ID + SM): 163// "@RG\tID:<id>\tSM:<sample>\n" 164// str_buf = id || sample (concatenated) 165// str_lens = [id_len, sample_len] 166// Optional @RG fields (PL platform / LB library / DT date / CN center 167// / DS description) are G6.0a.3b. 168func sam_write_rg_line(str_buf: *u8, 169 str_lens: *i64, 170 out_ascii: *u8, max_out: i64) -> i64 { 171 let id_len: i64 = str_lens[NX_SAM_RG_ID] 172 let sm_len: i64 = str_lens[NX_SAM_RG_SAMPLE] 173 if id_len < 1 { return -1 } 174 if sm_len < 1 { return -1 } 175 if max_out <= 0 { return -1 } 176 177 let off_id: i64 = 0 178 let off_sm: i64 = id_len 179 let id_ptr: *u8 = ((str_buf as i64) + off_id) as *u8 180 let sm_ptr: *u8 = ((str_buf as i64) + off_sm) as *u8 181 182 var off: i64 = 0 183 184 // @RG 185 if off + 3 > max_out { return -1 } 186 out_ascii[off+0]=0x40; out_ascii[off+1]=0x52; out_ascii[off+2]=0x47 187 off = off + 3 188 189 // \t + ID: 190 if off + 4 > max_out { return -1 } 191 out_ascii[off+0]=0x09 192 out_ascii[off+1]=0x49; out_ascii[off+2]=0x44; out_ascii[off+3]=0x3A 193 off = off + 4 194 195 let n_id: i64 = nx_copy_bytes(id_ptr, id_len, out_ascii, off, max_out) 196 if n_id < 0 { return -1 } 197 off = off + n_id 198 199 // \t + SM: 200 if off + 4 > max_out { return -1 } 201 out_ascii[off+0]=0x09 202 out_ascii[off+1]=0x53; out_ascii[off+2]=0x4D; out_ascii[off+3]=0x3A // SM: 203 off = off + 4 204 205 let n_sm: i64 = nx_copy_bytes(sm_ptr, sm_len, out_ascii, off, max_out) 206 if n_sm < 0 { return -1 } 207 off = off + n_sm 208 209 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 210 off = off + 1 211 212 return off 213} 214 215// Emit one SAM @PG program-lineage header line: 216// "@PG\tID:<id>\tPN:<name>\tVN:<version>\n" 217// str_buf = id || name || version (concatenated) 218// str_lens = [id_len, name_len, version_len] 219func sam_write_pg_line(str_buf: *u8, 220 str_lens: *i64, 221 out_ascii: *u8, max_out: i64) -> i64 { 222 let id_len: i64 = str_lens[NX_SAM_PG_ID] 223 let pn_len: i64 = str_lens[NX_SAM_PG_NAME] 224 let vn_len: i64 = str_lens[NX_SAM_PG_VERSION] 225 if id_len < 1 { return -1 } 226 if pn_len < 1 { return -1 } 227 if vn_len < 1 { return -1 } 228 if max_out <= 0 { return -1 } 229 230 let off_id: i64 = 0 231 let off_pn: i64 = id_len 232 let off_vn: i64 = id_len + pn_len 233 let id_ptr: *u8 = ((str_buf as i64) + off_id) as *u8 234 let pn_ptr: *u8 = ((str_buf as i64) + off_pn) as *u8 235 let vn_ptr: *u8 = ((str_buf as i64) + off_vn) as *u8 236 237 var off: i64 = 0 238 239 // @PG 240 if off + 3 > max_out { return -1 } 241 out_ascii[off+0]=0x40; out_ascii[off+1]=0x50; out_ascii[off+2]=0x47 242 off = off + 3 243 244 // \t + ID: 245 if off + 4 > max_out { return -1 } 246 out_ascii[off+0]=0x09 247 out_ascii[off+1]=0x49; out_ascii[off+2]=0x44; out_ascii[off+3]=0x3A // ID: 248 off = off + 4 249 250 // id bytes 251 let n_id: i64 = nx_copy_bytes(id_ptr, id_len, out_ascii, off, max_out) 252 if n_id < 0 { return -1 } 253 off = off + n_id 254 255 // \t + PN: 256 if off + 4 > max_out { return -1 } 257 out_ascii[off+0]=0x09 258 out_ascii[off+1]=0x50; out_ascii[off+2]=0x4E; out_ascii[off+3]=0x3A // PN: 259 off = off + 4 260 261 // name bytes 262 let n_pn: i64 = nx_copy_bytes(pn_ptr, pn_len, out_ascii, off, max_out) 263 if n_pn < 0 { return -1 } 264 off = off + n_pn 265 266 // \t + VN: 267 if off + 4 > max_out { return -1 } 268 out_ascii[off+0]=0x09 269 out_ascii[off+1]=0x56; out_ascii[off+2]=0x4E; out_ascii[off+3]=0x3A // VN: 270 off = off + 4 271 272 // version bytes 273 let n_vn: i64 = nx_copy_bytes(vn_ptr, vn_len, out_ascii, off, max_out) 274 if n_vn < 0 { return -1 } 275 off = off + n_vn 276 277 // \n 278 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 279 off = off + 1 280 281 return off 282} 283 284// Emit one SAM @SQ sequence-dictionary line for one reference contig: 285// "@SQ\tSN:<chrom>\tLN:<length>\n" 286// chrom_len bytes for the contig name; length is the contig's base count. 287func sam_write_sq_line(chrom: *u8, chrom_len: i64, 288 length: i64, 289 out_ascii: *u8, max_out: i64) -> i64 { 290 if chrom_len < 1 { return -1 } 291 if length < 1 { return -1 } 292 if max_out <= 0 { return -1 } 293 294 var off: i64 = 0 295 296 // @SQ 297 if off + 3 > max_out { return -1 } 298 out_ascii[off+0]=0x40; out_ascii[off+1]=0x53; out_ascii[off+2]=0x51 299 off = off + 3 300 301 // \t + SN: 302 if off + 4 > max_out { return -1 } 303 out_ascii[off+0]=0x09 304 out_ascii[off+1]=0x53; out_ascii[off+2]=0x4E; out_ascii[off+3]=0x3A // SN: 305 off = off + 4 306 307 // chrom bytes 308 let n_chrom: i64 = nx_copy_bytes(chrom, chrom_len, out_ascii, off, max_out) 309 if n_chrom < 0 { return -1 } 310 off = off + n_chrom 311 312 // \t + LN: 313 if off + 4 > max_out { return -1 } 314 out_ascii[off+0]=0x09 315 out_ascii[off+1]=0x4C; out_ascii[off+2]=0x4E; out_ascii[off+3]=0x3A // LN: 316 off = off + 4 317 318 // length digits 319 let n_len: i64 = nx_emit_int(length, out_ascii, off, max_out) 320 if n_len < 0 { return -1 } 321 off = off + n_len 322 323 // \n 324 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 325 off = off + 1 326 327 return off 328} 329 330// Emit a signed integer (handles negative TLEN per SAM spec). 331// For non-negative n, identical to nx_emit_int. For negative n, 332// writes a leading '-' then the absolute value. Returns bytes written 333// or -1 on overflow. 334func nx_emit_signed_int(n: i64, dst: *u8, dst_off: i64, max_dst: i64) -> i64 { 335 if n >= 0 { return nx_emit_int(n, dst, dst_off, max_dst) } 336 if dst_off + 1 > max_dst { return -1 } 337 dst[dst_off] = NX_ASCII_HYPHEN & 0xff 338 let n_int: i64 = nx_emit_int(0 - n, dst, dst_off + 1, max_dst) 339 if n_int < 0 { return -1 } 340 return n_int + 1 341} 342 343// Append a SAM optional integer tag (TYPE='i') to an existing line. 344// Format: "\tXX:i:<value>" (tab-separator before the tag) 345// 346// Common SAM-spec tags this enables: 347// NM:i:N edit distance to reference (per SAM v1 spec) 348// AS:i:N alignment score 349// NH:i:N number of reported hits for this read 350// HI:i:N query hit index (1..NH) 351// MQ:i:N mate mapping quality (paired-end) 352// XS:i:N suboptimal alignment score (BWA convention) 353// 354// Caller chains multiple tags by passing the returned offset back: 355// off = sam_append_tag_int(buf, off, max, 'N','M', 5) 356// off = sam_append_tag_int(buf, off, max, 'A','S', 48) 357// 358// Deferred: G6.0c.2 sam_append_tag_str (TYPE='Z' string), G6.0c.3 359// sam_append_tag_float (TYPE='f'), G6.0c.4 array tags (TYPE='B'). 360func sam_append_tag_int(out_ascii: *u8, off: i64, max_out: i64, 361 tag_char_1: i64, tag_char_2: i64, 362 value: i64) -> i64 { 363 if off < 0 { return -1 } 364 if max_out <= 0 { return -1 } 365 366 var cur_off: i64 = off 367 368 // Need at least 6 bytes for "\tXX:i:" prefix. 369 if cur_off + 6 > max_out { return -1 } 370 out_ascii[cur_off] = NX_ASCII_TAB & 0xff 371 out_ascii[cur_off+1] = tag_char_1 & 0xff 372 out_ascii[cur_off+2] = tag_char_2 & 0xff 373 out_ascii[cur_off+3] = 0x3A // ':' 374 out_ascii[cur_off+4] = 0x69 // 'i' 375 out_ascii[cur_off+5] = 0x3A // ':' 376 cur_off = cur_off + 6 377 378 let n_val: i64 = nx_emit_signed_int(value, out_ascii, cur_off, max_out) 379 if n_val < 0 { return -1 } 380 cur_off = cur_off + n_val 381 382 return cur_off 383} 384 385// Append a SAM optional string tag (TYPE='Z') to an existing line. 386// Format: "\tXX:Z:<string>" (tab-separator before the tag) 387// 388// Common SAM-spec Z-type tags this enables: 389// RG:Z:<id> read-group linkback to @RG header line 390// MD:Z:<string> mismatching positions (samtools convention) 391// PG:Z:<id> program-group lineage 392// XA:Z:<...> alternative alignments (BWA convention) 393// BC:Z:<...> barcode sequence (Illumina convention) 394// 395// Per SAM v1 spec, Z-type values are printable ASCII (and SPACE) -- 396// no internal tabs or NULs. This primitive does NOT validate value 397// bytes; caller is responsible for content discipline. 398func sam_append_tag_str(out_ascii: *u8, off: i64, max_out: i64, 399 tag_char_1: i64, tag_char_2: i64, 400 str_ptr: *u8, str_len: i64) -> i64 { 401 if off < 0 { return -1 } 402 if str_len < 0 { return -1 } 403 if max_out <= 0 { return -1 } 404 405 var cur_off: i64 = off 406 407 if cur_off + 6 > max_out { return -1 } 408 out_ascii[cur_off] = NX_ASCII_TAB & 0xff 409 out_ascii[cur_off+1] = tag_char_1 & 0xff 410 out_ascii[cur_off+2] = tag_char_2 & 0xff 411 out_ascii[cur_off+3] = 0x3A 412 out_ascii[cur_off+4] = 0x5A // 'Z' 413 out_ascii[cur_off+5] = 0x3A 414 cur_off = cur_off + 6 415 416 let n_str: i64 = nx_copy_bytes(str_ptr, str_len, out_ascii, cur_off, max_out) 417 if n_str < 0 { return -1 } 418 cur_off = cur_off + n_str 419 420 return cur_off 421} 422 423// Emit one SAM v1.4.6 data line per spec. See header for API. 424func sam_write_line(str_buf: *u8, 425 str_lens: *i64, 426 fields: *i64, 427 out_ascii: *u8, max_out: i64) -> i64 { 428 let qname_len: i64 = str_lens[NX_SAM_STR_QNAME] 429 let rname_len: i64 = str_lens[NX_SAM_STR_RNAME] 430 let cigar_len: i64 = str_lens[NX_SAM_STR_CIGAR] 431 let seq_len: i64 = str_lens[NX_SAM_STR_SEQ] 432 let flag: i64 = fields[NX_SAM_FIELD_FLAG] 433 let pos: i64 = fields[NX_SAM_FIELD_POS] 434 let mapq: i64 = fields[NX_SAM_FIELD_MAPQ] 435 let tlen: i64 = fields[NX_SAM_FIELD_TLEN] 436 437 if qname_len < 1 { return -1 } 438 if rname_len < 1 { return -1 } 439 if cigar_len < 1 { return -1 } 440 if seq_len < 1 { return -1 } 441 if flag < 0 { return -1 } 442 if pos < 1 { return -1 } 443 if mapq < 0 { return -1 } 444 if max_out <= 0 { return -1 } 445 446 // Compute string offsets within str_buf. 447 let off_qname: i64 = 0 448 let off_rname: i64 = off_qname + qname_len 449 let off_cigar: i64 = off_rname + rname_len 450 let off_seq: i64 = off_cigar + cigar_len 451 let off_qual: i64 = off_seq + seq_len 452 let qname_ptr: *u8 = ((str_buf as i64) + off_qname) as *u8 453 let rname_ptr: *u8 = ((str_buf as i64) + off_rname) as *u8 454 let cigar_ptr: *u8 = ((str_buf as i64) + off_cigar) as *u8 455 let seq_ptr: *u8 = ((str_buf as i64) + off_seq) as *u8 456 let qual_ptr: *u8 = ((str_buf as i64) + off_qual) as *u8 457 458 var off: i64 = 0 459 460 // ---- QNAME ---- 461 let n_qname: i64 = nx_copy_bytes(qname_ptr, qname_len, out_ascii, off, max_out) 462 if n_qname < 0 { return -1 } 463 off = off + n_qname 464 465 // ---- tab + FLAG ---- 466 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 467 off = off + 1 468 let n_flag: i64 = nx_emit_int(flag, out_ascii, off, max_out) 469 if n_flag < 0 { return -1 } 470 off = off + n_flag 471 472 // ---- tab + RNAME ---- 473 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 474 off = off + 1 475 let n_rname: i64 = nx_copy_bytes(rname_ptr, rname_len, out_ascii, off, max_out) 476 if n_rname < 0 { return -1 } 477 off = off + n_rname 478 479 // ---- tab + POS ---- 480 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 481 off = off + 1 482 let n_pos: i64 = nx_emit_int(pos, out_ascii, off, max_out) 483 if n_pos < 0 { return -1 } 484 off = off + n_pos 485 486 // ---- tab + MAPQ ---- 487 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 488 off = off + 1 489 let n_mapq: i64 = nx_emit_int(mapq, out_ascii, off, max_out) 490 if n_mapq < 0 { return -1 } 491 off = off + n_mapq 492 493 // ---- tab + CIGAR ---- 494 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 495 off = off + 1 496 let n_cigar: i64 = nx_copy_bytes(cigar_ptr, cigar_len, out_ascii, off, max_out) 497 if n_cigar < 0 { return -1 } 498 off = off + n_cigar 499 500 // ---- tab + RNEXT '*' ---- 501 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 502 off = off + 1 503 if nx_copy_byte(0x2A, out_ascii, off, max_out) < 0 { return -1 } // '*' 504 off = off + 1 505 506 // ---- tab + PNEXT 0 ---- 507 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 508 off = off + 1 509 if nx_copy_byte(NX_ASCII_DIGIT_0, out_ascii, off, max_out) < 0 { return -1 } 510 off = off + 1 511 512 // ---- tab + TLEN ---- 513 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 514 off = off + 1 515 let n_tlen: i64 = nx_emit_signed_int(tlen, out_ascii, off, max_out) 516 if n_tlen < 0 { return -1 } 517 off = off + n_tlen 518 519 // ---- tab + SEQ ---- 520 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 521 off = off + 1 522 let n_seq: i64 = nx_copy_bytes(seq_ptr, seq_len, out_ascii, off, max_out) 523 if n_seq < 0 { return -1 } 524 off = off + n_seq 525 526 // ---- tab + QUAL ---- 527 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 528 off = off + 1 529 let n_qual: i64 = nx_copy_bytes(qual_ptr, seq_len, out_ascii, off, max_out) 530 if n_qual < 0 { return -1 } 531 off = off + n_qual 532 533 // ---- terminating LF ---- 534 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 535 off = off + 1 536 537 return off 538} 539 540// G6.0b -- paired-end SAM data line writer. 541// 542// Mirrors sam_write_line API + adds mate fields (RNEXT/PNEXT/TLEN). 543// RNEXT is a STRING (mate's reference name, or "=" for same as 544// RNAME, or "*" for unmapped) so it lives in the bundled string 545// buffer alongside qname/rname/cigar/seq/qual. 546// 547// str_buf = qname || rname || rnext || cigar || seq || qual 548// str_lens = [qname_len, rname_len, rnext_len, cigar_len, seq_len] 549// (qual_len == seq_len per SAM-spec invariant) 550// fields = [flag, pos_1indexed, mapq, pnext_1indexed, tlen] 551// (TLEN is signed; SAM spec: positive for leftmost mate, 552// negative for rightmost; 0 if undefined) 553// 554// 5-arg API per the recurring nxc2 codegen quirk. 555// 556// Example: typical proper-pair FR read: 557// FLAG=99 (paired+proper+mate_rev+first), RNAME=chr1, POS=100, 558// MAPQ=60, CIGAR=3M, RNEXT="=", PNEXT=250, TLEN=153, SEQ=ACG, QUAL=III 559// -> "r1\t99\tchr1\t100\t60\t3M\t=\t250\t153\tACG\tIII\n" 560func sam_write_line_paired(str_buf: *u8, 561 str_lens: *i64, 562 fields: *i64, 563 out_ascii: *u8, max_out: i64) -> i64 { 564 let qname_len: i64 = str_lens[NX_SAM_PE_STR_QNAME] 565 let rname_len: i64 = str_lens[NX_SAM_PE_STR_RNAME] 566 let rnext_len: i64 = str_lens[NX_SAM_PE_STR_RNEXT] 567 let cigar_len: i64 = str_lens[NX_SAM_PE_STR_CIGAR] 568 let seq_len: i64 = str_lens[NX_SAM_PE_STR_SEQ] 569 let flag: i64 = fields[NX_SAM_PE_FIELD_FLAG] 570 let pos: i64 = fields[NX_SAM_PE_FIELD_POS] 571 let mapq: i64 = fields[NX_SAM_PE_FIELD_MAPQ] 572 let pnext: i64 = fields[NX_SAM_PE_FIELD_PNEXT] 573 let tlen: i64 = fields[NX_SAM_PE_FIELD_TLEN] 574 575 if qname_len < 1 { return -1 } 576 if rname_len < 1 { return -1 } 577 if rnext_len < 1 { return -1 } 578 if cigar_len < 1 { return -1 } 579 if seq_len < 1 { return -1 } 580 if flag < 0 { return -1 } 581 if pos < 1 { return -1 } 582 if mapq < 0 { return -1 } 583 if pnext < 0 { return -1 } 584 if max_out <= 0 { return -1 } 585 586 let off_qname: i64 = 0 587 let off_rname: i64 = off_qname + qname_len 588 let off_rnext: i64 = off_rname + rname_len 589 let off_cigar: i64 = off_rnext + rnext_len 590 let off_seq: i64 = off_cigar + cigar_len 591 let off_qual: i64 = off_seq + seq_len 592 let qname_ptr: *u8 = ((str_buf as i64) + off_qname) as *u8 593 let rname_ptr: *u8 = ((str_buf as i64) + off_rname) as *u8 594 let rnext_ptr: *u8 = ((str_buf as i64) + off_rnext) as *u8 595 let cigar_ptr: *u8 = ((str_buf as i64) + off_cigar) as *u8 596 let seq_ptr: *u8 = ((str_buf as i64) + off_seq) as *u8 597 let qual_ptr: *u8 = ((str_buf as i64) + off_qual) as *u8 598 599 var off: i64 = 0 600 601 let n_qname: i64 = nx_copy_bytes(qname_ptr, qname_len, out_ascii, off, max_out) 602 if n_qname < 0 { return -1 } 603 off = off + n_qname 604 605 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 606 off = off + 1 607 let n_flag: i64 = nx_emit_int(flag, out_ascii, off, max_out) 608 if n_flag < 0 { return -1 } 609 off = off + n_flag 610 611 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 612 off = off + 1 613 let n_rname: i64 = nx_copy_bytes(rname_ptr, rname_len, out_ascii, off, max_out) 614 if n_rname < 0 { return -1 } 615 off = off + n_rname 616 617 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 618 off = off + 1 619 let n_pos: i64 = nx_emit_int(pos, out_ascii, off, max_out) 620 if n_pos < 0 { return -1 } 621 off = off + n_pos 622 623 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 624 off = off + 1 625 let n_mapq: i64 = nx_emit_int(mapq, out_ascii, off, max_out) 626 if n_mapq < 0 { return -1 } 627 off = off + n_mapq 628 629 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 630 off = off + 1 631 let n_cigar: i64 = nx_copy_bytes(cigar_ptr, cigar_len, out_ascii, off, max_out) 632 if n_cigar < 0 { return -1 } 633 off = off + n_cigar 634 635 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 636 off = off + 1 637 let n_rnext: i64 = nx_copy_bytes(rnext_ptr, rnext_len, out_ascii, off, max_out) 638 if n_rnext < 0 { return -1 } 639 off = off + n_rnext 640 641 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 642 off = off + 1 643 let n_pnext: i64 = nx_emit_int(pnext, out_ascii, off, max_out) 644 if n_pnext < 0 { return -1 } 645 off = off + n_pnext 646 647 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 648 off = off + 1 649 let n_tlen: i64 = nx_emit_signed_int(tlen, out_ascii, off, max_out) 650 if n_tlen < 0 { return -1 } 651 off = off + n_tlen 652 653 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 654 off = off + 1 655 let n_seq: i64 = nx_copy_bytes(seq_ptr, seq_len, out_ascii, off, max_out) 656 if n_seq < 0 { return -1 } 657 off = off + n_seq 658 659 if nx_copy_byte(NX_ASCII_TAB, out_ascii, off, max_out) < 0 { return -1 } 660 off = off + 1 661 let n_qual: i64 = nx_copy_bytes(qual_ptr, seq_len, out_ascii, off, max_out) 662 if n_qual < 0 { return -1 } 663 off = off + n_qual 664 665 if nx_copy_byte(NX_ASCII_LF, out_ascii, off, max_out) < 0 { return -1 } 666 off = off + 1 667 668 return off 669}