nx_bam.nx source
↩ module page · 278 lines · 11162 B
1// nx_bam.nx -- BAM binary record serialiser (SAMv1 §4).
2//
3// G6.0d.1: nx_bam_pack_seq -- 4-bit IUPAC nucleotide packing.
4// Each base in the ASCII input is mapped to a 4-bit code per
5// SAMv1 §4.2 Table 1 (= 0, A=1, C=2, M=3, G=4, R=5, S=6,
6// V=7, T=8, W=9, Y=10, H=11, K=12, D=13, B=14, N=15).
7// Bases are packed two-per-byte, HIGH nibble first.
8// Output length: ceil(seq_len / 2) bytes.
9// On odd seq_len, last low nibble is zero per spec.
10// Unrecognised ASCII -> NX_BAM_NT_N (15) -- conservative.
11//
12// G6.0d.2: nx_bam_pack_cigar -- pack CIGAR ops into u32 LE words.
13// Each op packs as (op_len << 4) | op_code; written little-endian.
14// Input: parallel arrays ops[] + lens[] of n_op entries.
15// Output: n_op * 4 bytes.
16//
17// G6.0d.3: nx_bam_write_record -- canonical BAM alignment record.
18// block_size + 32-byte fixed header + read_name + cigar + seq + qual
19// per SAMv1 §4.2. Header fields written little-endian via nx_le.
20//
21// expect_exit: 0
22//
23// license_tier: ORIGINAL
24
25import "nx_syscalls.nx"
26import "nx_const.nx"
27import "nx_le.nx"
28const NX_MAGIC_65535: i64 = 65535
29
30// ASCII -> 4-bit BAM code. Case-insensitive (upper + lower mapped).
31// Anything not recognised -> NX_BAM_NT_N (conservative; preserves
32// downstream usability rather than failing the encode).
33func nx_bam_iupac_to_nibble(c: i64) -> i64 {
34 let ch: i64 = c & 0xff
35 if ch == 0x3D { return NX_BAM_NT_EQ } // =
36 if ch == 0x41 { return NX_BAM_NT_A } // A
37 if ch == 0x61 { return NX_BAM_NT_A } // a
38 if ch == 0x43 { return NX_BAM_NT_C } // C
39 if ch == 0x63 { return NX_BAM_NT_C } // c
40 if ch == 0x4D { return NX_BAM_NT_M } // M
41 if ch == 0x6D { return NX_BAM_NT_M } // m
42 if ch == 0x47 { return NX_BAM_NT_G } // G
43 if ch == 0x67 { return NX_BAM_NT_G } // g
44 if ch == 0x52 { return NX_BAM_NT_R } // R
45 if ch == 0x72 { return NX_BAM_NT_R } // r
46 if ch == 0x53 { return NX_BAM_NT_S } // S
47 if ch == 0x73 { return NX_BAM_NT_S } // s
48 if ch == 0x56 { return NX_BAM_NT_V } // V
49 if ch == 0x76 { return NX_BAM_NT_V } // v
50 if ch == 0x54 { return NX_BAM_NT_T } // T
51 if ch == 0x74 { return NX_BAM_NT_T } // t
52 if ch == 0x55 { return NX_BAM_NT_T } // U -> T (RNA collapsed to DNA)
53 if ch == 0x75 { return NX_BAM_NT_T } // u
54 if ch == 0x57 { return NX_BAM_NT_W } // W
55 if ch == 0x77 { return NX_BAM_NT_W } // w
56 if ch == 0x59 { return NX_BAM_NT_Y } // Y
57 if ch == 0x79 { return NX_BAM_NT_Y } // y
58 if ch == 0x48 { return NX_BAM_NT_H } // H
59 if ch == 0x68 { return NX_BAM_NT_H } // h
60 if ch == 0x4B { return NX_BAM_NT_K } // K
61 if ch == 0x6B { return NX_BAM_NT_K } // k
62 if ch == 0x44 { return NX_BAM_NT_D } // D
63 if ch == 0x64 { return NX_BAM_NT_D } // d
64 if ch == 0x42 { return NX_BAM_NT_B } // B
65 if ch == 0x62 { return NX_BAM_NT_B } // b
66 return NX_BAM_NT_N // N or any unrecognised
67}
68
69// G6.0d.1 -- pack ASCII IUPAC sequence into BAM 4-bit nibbles.
70// Returns number of bytes written, or -1 on error.
71func nx_bam_pack_seq(seq_ascii: *u8, seq_len: i64,
72 out_bytes: *u8, max_out: i64) -> i64 {
73 if seq_len < 0 { return -1 }
74 if max_out < 0 { return -1 }
75
76 let n_bytes: i64 = (seq_len + 1) / 2
77 if n_bytes > max_out { return -1 }
78
79 var i: i64 = 0
80 while i < n_bytes {
81 let hi_idx: i64 = i * 2
82 let lo_idx: i64 = hi_idx + 1
83 let hi: i64 = nx_bam_iupac_to_nibble(seq_ascii[hi_idx] as i64)
84 var lo: i64 = 0
85 if lo_idx < seq_len {
86 lo = nx_bam_iupac_to_nibble(seq_ascii[lo_idx] as i64)
87 }
88 let byte_val: i64 = ((hi & 0xf) << 4) | (lo & 0xf)
89 out_bytes[i] = byte_val as u8
90 i = i + 1
91 }
92
93 return n_bytes
94}
95
96// Field-array indices for nx_bam_write_record's bundled args.
97// Caller passes a 12-entry i64[] in this order (3 length fields
98// folded in to keep the function-arg count at the nxc2 codegen
99// safe budget):
100const NX_BAM_REC_REF_ID: i64 = 0
101const NX_BAM_REC_POS: i64 = 1 // 0-indexed (NOT 1-indexed like SAM)
102const NX_BAM_REC_MAPQ: i64 = 2
103const NX_BAM_REC_BIN: i64 = 3 // UCSC bin (0 for unbinned)
104const NX_BAM_REC_FLAG: i64 = 4
105const NX_BAM_REC_NEXT_REF: i64 = 5
106const NX_BAM_REC_NEXT_POS: i64 = 6 // 0-indexed
107const NX_BAM_REC_TLEN: i64 = 7 // signed
108const NX_BAM_REC_NO_QUAL: i64 = 8 // 0 = qual present, 1 = fill 0xFF
109const NX_BAM_REC_READ_NAME_LEN: i64 = 9 // bytes (NUL appended internally)
110const NX_BAM_REC_N_CIGAR_OP: i64 = 10
111const NX_BAM_REC_L_SEQ: i64 = 11
112
113// G6.0d.2 -- pack CIGAR ops into BAM u32 LE words.
114// Each op encodes as (op_len << 4) | op_code.
115// Returns total bytes written (n_op * 4), or -1 on error.
116func nx_bam_pack_cigar(ops: *i64, lens: *i64, n_op: i64,
117 out_bytes: *u8, max_out: i64) -> i64 {
118 if n_op < 0 { return -1 }
119 if max_out < 0 { return -1 }
120 let n_bytes: i64 = n_op * 4
121 if n_bytes > max_out { return -1 }
122
123 var i: i64 = 0
124 while i < n_op {
125 let op: i64 = ops[i]
126 let lng: i64 = lens[i]
127 if op < 0 { return -1 }
128 if op > 8 { return -1 } // valid CIGAR op codes 0..8 per spec
129 if lng < 0 { return -1 }
130 let packed: i64 = (lng << 4) | (op & 0xf)
131 let off: i64 = i * 4
132 let r: i64 = nx_le_write_u32(out_bytes, off, packed)
133 if r < 0 { return -1 }
134 i = i + 1
135 }
136
137 return n_bytes
138}
139
140// G6.0d.3 -- write a full BAM alignment record per SAMv1 §4.2.
141//
142// Wire layout (every multi-byte field little-endian):
143// [0..4) block_size (i32) -- bytes 4..end (i.e., total - 4)
144// [4..8) refID (i32, -1 = *)
145// [8..12) pos (i32, 0-indexed; -1 = unmapped)
146// [12] l_read_name (u8, includes trailing NUL)
147// [13] mapq (u8)
148// [14..16) bin (u16; 0 acceptable if not binned)
149// [16..18) n_cigar_op (u16)
150// [18..20) flag (u16)
151// [20..24) l_seq (u32)
152// [24..28) next_refID (i32)
153// [28..32) next_pos (i32, 0-indexed)
154// [32..36) tlen (i32, signed)
155// [36..] read_name (l_read_name bytes, NUL-terminated)
156// + cigar (n_cigar_op * 4 bytes)
157// + seq ((l_seq+1)/2 bytes packed)
158// + qual (l_seq bytes; 0xFF * l_seq if NO_QUAL=1)
159//
160// Bundled-args API per the recurring nxc2 codegen quirk (8 args):
161// read_name_ascii -- WITHOUT trailing NUL (we append one)
162// cigar_ops/lens -- arrays of length fields[N_CIGAR_OP]
163// seq_ascii -- IUPAC string, length = fields[L_SEQ]
164// qual_phred -- raw Phred bytes (NOT ASCII +33), length = L_SEQ.
165// Ignored when fields[NX_BAM_REC_NO_QUAL] == 1.
166// fields -- 12-entry i64 array indexed by NX_BAM_REC_*
167// (carries the 3 length counts so the function
168// arg count stays at 8).
169//
170// Returns total bytes written (including the 4-byte block_size
171// prefix), or -1 on validation / capacity failure.
172func nx_bam_write_record(read_name_ascii: *u8,
173 cigar_ops: *i64, cigar_lens: *i64,
174 seq_ascii: *u8,
175 qual_phred: *u8,
176 fields: *i64,
177 out_bytes: *u8, max_out: i64) -> i64 {
178 let read_name_len: i64 = fields[NX_BAM_REC_READ_NAME_LEN]
179 let n_cigar_op: i64 = fields[NX_BAM_REC_N_CIGAR_OP]
180 let l_seq: i64 = fields[NX_BAM_REC_L_SEQ]
181
182 if read_name_len < 1 { return -1 }
183 if n_cigar_op < 0 { return -1 }
184 if l_seq < 0 { return -1 }
185 if max_out < 36 { return -1 } // even an empty record has 36-byte hdr
186
187 let l_read_name: i64 = read_name_len + 1 // BAM spec: includes NUL
188 if l_read_name > 255 { return -1 } // u8 cap
189
190 let cigar_bytes: i64 = n_cigar_op * 4
191 let seq_bytes: i64 = (l_seq + 1) / 2
192 let qual_bytes: i64 = l_seq
193
194 // total bytes including the leading 4-byte block_size field itself.
195 let total: i64 = 4 + 32 + l_read_name + cigar_bytes + seq_bytes + qual_bytes
196 if total > max_out { return -1 }
197
198 // block_size excludes the 4-byte block_size field itself.
199 let block_size: i64 = total - 4
200
201 // Reject obvious out-of-range u16 fields up front.
202 let bin: i64 = fields[NX_BAM_REC_BIN]
203 let flag: i64 = fields[NX_BAM_REC_FLAG]
204 if bin < 0 { return -1 }
205 if bin > NX_MAGIC_65535 { return -1 }
206 if flag < 0 { return -1 }
207 if flag > NX_MAGIC_65535 { return -1 }
208 if n_cigar_op > NX_MAGIC_65535 { return -1 }
209
210 let mapq: i64 = fields[NX_BAM_REC_MAPQ]
211 if mapq < 0 { return -1 }
212 if mapq > 255 { return -1 }
213
214 let no_qual: i64 = fields[NX_BAM_REC_NO_QUAL]
215
216 // ---- fixed-size header ----
217 if nx_le_write_u32(out_bytes, 0, block_size) < 0 { return -1 }
218 if nx_le_write_u32(out_bytes, 4, fields[NX_BAM_REC_REF_ID]) < 0 { return -1 }
219 if nx_le_write_u32(out_bytes, 8, fields[NX_BAM_REC_POS]) < 0 { return -1 }
220 out_bytes[12] = l_read_name as u8
221 out_bytes[13] = mapq as u8
222 if nx_le_write_u16(out_bytes, 14, bin) < 0 { return -1 }
223 if nx_le_write_u16(out_bytes, 16, n_cigar_op) < 0 { return -1 }
224 if nx_le_write_u16(out_bytes, 18, flag) < 0 { return -1 }
225 if nx_le_write_u32(out_bytes, 20, l_seq) < 0 { return -1 }
226 if nx_le_write_u32(out_bytes, 24, fields[NX_BAM_REC_NEXT_REF]) < 0 { return -1 }
227 if nx_le_write_u32(out_bytes, 28, fields[NX_BAM_REC_NEXT_POS]) < 0 { return -1 }
228 if nx_le_write_u32(out_bytes, 32, fields[NX_BAM_REC_TLEN]) < 0 { return -1 }
229
230 // ---- variable sections ----
231 var off: i64 = 36
232
233 // read_name (NUL-terminated)
234 var i: i64 = 0
235 while i < read_name_len {
236 out_bytes[off + i] = read_name_ascii[i]
237 i = i + 1
238 }
239 out_bytes[off + read_name_len] = 0 // trailing NUL
240 off = off + l_read_name
241
242 // cigar
243 if n_cigar_op > 0 {
244 let cig_dst: *u8 = ((out_bytes as i64) + off) as *u8
245 let cn: i64 = nx_bam_pack_cigar(cigar_ops, cigar_lens, n_cigar_op,
246 cig_dst, cigar_bytes)
247 if cn != cigar_bytes { return -1 }
248 off = off + cigar_bytes
249 }
250
251 // seq
252 if l_seq > 0 {
253 let seq_dst: *u8 = ((out_bytes as i64) + off) as *u8
254 let sn: i64 = nx_bam_pack_seq(seq_ascii, l_seq, seq_dst, seq_bytes)
255 if sn != seq_bytes { return -1 }
256 off = off + seq_bytes
257 }
258
259 // qual
260 if l_seq > 0 {
261 if no_qual == 1 {
262 var qi: i64 = 0
263 while qi < l_seq {
264 out_bytes[off + qi] = NX_BAM_NO_QUAL_BYTE as u8
265 qi = qi + 1
266 }
267 } else {
268 var qj: i64 = 0
269 while qj < l_seq {
270 out_bytes[off + qj] = qual_phred[qj]
271 qj = qj + 1
272 }
273 }
274 off = off + l_seq
275 }
276
277 return off // total bytes written; equals `total` above.
278}