nx_fastq.nx source
↩ module page · 147 lines · 6125 B
1// nx_fastq.nx -- FASTQ single-record stream parser.
2//
3// license_tier: INDEPENDENT_REDERIVE
4// genealogy_id: international-research-sources/cock-2010-fastq-format
5//
6// G2.0b of NISHI_GENOMICS_SUBSTRATE_ROADMAP.md. The bridge between
7// raw sequencer-emitted byte streams and the substrate's typed DNA
8// + Phred primitives. Parses ONE record per call (4-line format)
9// and emits offset metadata; the caller composes downstream with:
10//
11// dna_pack_ambig(buf + seq_start, seq_len, out_bases, out_nbits)
12// nx_phred33_decode_string(buf + qual_start, qual_len, out_qs)
13//
14// FASTQ canonical 4-line format (Cock 2010):
15// line 1: '@' + identifier [+ optional description]
16// line 2: nucleotide sequence (single line in G2.0b -- see deferred)
17// line 3: '+' [+ optional repeated identifier]
18// line 4: quality string, len == seq len, Phred+33 encoded
19//
20// Validation enforced:
21// - record starts with '@' (offset position 0)
22// - separator line starts with '+'
23// - seq_len == qual_len
24//
25// What G2.0b does NOT do (deferred):
26// - Multi-line sequence / quality (rare in modern data; complicates
27// parsing because qual chars include '@' and '+' so line-by-line
28// scanning ambiguates without lookahead) -- G2.0b.2
29// - gzip / bgzip decompression -- callers pre-decompress; nx_gzip
30// wrapper exists, integration is G2.0b.3
31// - Header field parsing (Illumina-style "INSTRUMENT:RUN:FLOWCELL:..."
32// metadata) -- G2.0b.4
33// - Phred encoding auto-detection (+33 vs +64) -- G2.0a.1 heuristic
34// - Paired-end record pairing across R1/R2 files -- G2.0b.5
35//
36// API:
37// nx_find_lf(buf, start, end) -> i64 offset of next LF or -1
38// fastq_parse_one_record(buf, buf_len, offset, out_meta) -> i64
39//
40// out_meta layout (7 i64s):
41// [0] header_start (offset after the '@', start of identifier)
42// [1] header_len (bytes from header_start to end of line 1)
43// [2] seq_start
44// [3] seq_len
45// [4] qual_start
46// [5] qual_len (== seq_len, validated)
47// [6] next_offset (where the next record begins; == buf_len if last)
48//
49// Return codes:
50// 0 = success
51// 1 = EOF / no record at this offset
52// -1 = malformed record (bad markers, length mismatch, truncated)
53//
54// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml)
55// intended_use: "Stream-parse FASTQ records from a byte
56// buffer; the entry point for real-world
57// sequencer data into the alignment pipeline"
58// sil_target: SIL2
59// asil_target: QM
60// dal_target: DAL C
61// iec_62304_class: B
62// evidence: [no_floating_point, deterministic,
63// bit_equal_reproducible,
64// cock_2010_4_line_canonical_format,
65// composes_nx_const_ASCII_markers,
66// composes_nx_sequence_ambig_and_nx_phred,
67// seq_qual_length_invariant_enforced,
68// license_tier_INDEPENDENT_REDERIVE]
69// hazard_register: [bug-tape-fastq-multi-line-qual-confused-with-record-start,
70// bug-tape-fastq-seq-qual-length-mismatch-silent,
71// bug-tape-fastq-cr-lf-windows-line-ending-off-by-one,
72// bug-tape-fastq-truncated-trailing-record-not-flagged,
73// bug-tape-fastq-empty-record-returned-as-zero-len-not-skipped]
74// residual_risk: "G2.0b assumes single-line seq + qual.
75// Modern Illumina / ONT / PacBio FASTQ
76// always single-line; legacy multi-line
77// from old NCBI dumps will fail with -1
78// and require G2.0b.2. CRLF line endings
79// not stripped; caller should normalise."
80// verdict: NOT_YET_EVALUATED
81
82import "nx_syscalls.nx"
83import "nx_const.nx"
84
85// Find the next LF (0x0A) byte in [start, end). Returns the offset
86// of the LF, or -1 if none found.
87func nx_find_lf(buf: *u8, start: i64, end: i64) -> i64 {
88 var i: i64 = start
89 while i < end {
90 if (buf[i] & 0xff) == NX_ASCII_LF { return i }
91 i = i + 1
92 }
93 return -1
94}
95
96// Parse one FASTQ record beginning at `offset`. See header for
97// metadata layout and return codes.
98func fastq_parse_one_record(buf: *u8, buf_len: i64, offset: i64,
99 out_meta: *i64) -> i64 {
100 if buf_len < 0 { return -1 }
101 if offset < 0 { return -1 }
102 if offset >= buf_len { return 1 } // EOF
103
104 // ---- Line 1: '@' header ----
105 if (buf[offset] & 0xff) != NX_ASCII_AT { return -1 }
106 let header_start: i64 = offset + 1
107 let lf1: i64 = nx_find_lf(buf, header_start, buf_len)
108 if lf1 < 0 { return -1 }
109 let header_len: i64 = lf1 - header_start
110
111 // ---- Line 2: sequence ----
112 let seq_start: i64 = lf1 + 1
113 if seq_start >= buf_len { return -1 }
114 let lf2: i64 = nx_find_lf(buf, seq_start, buf_len)
115 if lf2 < 0 { return -1 }
116 let seq_len: i64 = lf2 - seq_start
117
118 // ---- Line 3: '+' separator ----
119 let sep_start: i64 = lf2 + 1
120 if sep_start >= buf_len { return -1 }
121 if (buf[sep_start] & 0xff) != NX_ASCII_PLUS { return -1 }
122 let lf3: i64 = nx_find_lf(buf, sep_start, buf_len)
123 if lf3 < 0 { return -1 }
124
125 // ---- Line 4: quality ----
126 let qual_start: i64 = lf3 + 1
127 if qual_start > buf_len { return -1 }
128 // qual_len must equal seq_len; we read exactly that many bytes
129 // and the next byte must be either LF or EOF.
130 let qual_end: i64 = qual_start + seq_len
131 if qual_end > buf_len { return -1 }
132
133 var next_offset: i64 = qual_end
134 if next_offset < buf_len {
135 if (buf[next_offset] & 0xff) != NX_ASCII_LF { return -1 }
136 next_offset = next_offset + 1
137 }
138
139 out_meta[0] = header_start
140 out_meta[1] = header_len
141 out_meta[2] = seq_start
142 out_meta[3] = seq_len
143 out_meta[4] = qual_start
144 out_meta[5] = seq_len // qual_len enforced equal to seq_len
145 out_meta[6] = next_offset
146 return 0
147}