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}