nx_csv_v1.nx source
↩ module page · 201 lines · 6701 B
1// csv.nx -- RFC 4180 comma-separated values parser.
2//
3// Companion to tsv.nx. CSV is messier than TSV because fields
4// may be quoted, quotes inside quotes are doubled, and the line
5// separator is CRLF per spec (most real-world files use plain
6// LF -- we accept both).
7//
8// RFC 4180 grammar (simplified):
9// file = header (CRLF record)* CRLF?
10// header/rec = field (, field)*
11// field = non_escaped | escaped
12// non_escaped = TEXTDATA*
13// escaped = DQUOTE (TEXTDATA | , | CR | LF | DQUOTE DQUOTE)* DQUOTE
14// DQUOTE = 0x22
15//
16// We emit a FIELD CALLBACK at each cell boundary and a ROW
17// CALLBACK at each newline. This avoids allocating a 2D table
18// and lets caller stream megabyte+ files.
19//
20// Invariants:
21// CSV1 Quoted fields handle embedded commas + newlines.
22// CSV2 Doubled quotes inside quoted fields ("") decode to one
23// literal DQUOTE -- caller handles via the callback (we
24// emit the raw quoted range; decoder below does the
25// unescape).
26// CSV3 Trailing CR before LF is stripped.
27// CSV4 Empty file -> 0 rows, 0 cells.
28
29// nx_safety_envelope:
30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
31// sil_target: SIL1
32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
33// verdict: NOT_YET_EVALUATED
34
35import "nx_syscalls.nx"
36const CSV_MAGIC_1024: i64 = 1024
37
38const CSV_ERR_OVERFLOW: i64 = -1
39const CSV_ERR_UNTERM: i64 = -2
40
41// Caller-friendly output: two arrays populated by the parser.
42// cells[i] = (start, end) offsets into buf
43// rows[j] contains the first cell index for row j + 1 (sentinel)
44// Row j spans cells[rows[j] .. rows[j+1] - 1].
45struct CsvCell {
46 start: i64,
47 end: i64,
48 // Whether cell was quoted -- caller may need to unescape "".
49 quoted: i64,
50}
51
52// Parse an entire CSV blob. Fills cells[] + rows[].
53// On success returns number of rows; rows[] length is result+1
54// (last entry is total cell count). Negative on error.
55func csv_parse(buf: *u8, n: i64,
56 cells: *CsvCell, cells_cap: i64,
57 rows: *i64, rows_cap: i64) -> i64 {
58 var cell_count: i64 = 0
59 var row_count: i64 = 0
60 // Always record row 0 start at cell 0.
61 if rows_cap > 0 { rows[0] = 0 }
62 var i: i64 = 0
63
64 while i < n {
65 // Start of a new cell.
66 if cell_count >= cells_cap { return CSV_ERR_OVERFLOW }
67 let cell: *CsvCell = cells + cell_count * 24
68
69 if buf[i] == 0x22 {
70 // Quoted field. Consume opening quote.
71 i = i + 1
72 cell.start = i
73 cell.quoted = 1
74 while i < n {
75 if buf[i] == 0x22 {
76 // Either end of field OR escaped "".
77 if i + 1 < n {
78 if buf[i + 1] == 0x22 {
79 // "" escape; consume both, continue.
80 i = i + 2
81 continue
82 }
83 }
84 // End of quoted field.
85 cell.end = i
86 i = i + 1
87 break
88 }
89 i = i + 1
90 }
91 if i > n { return CSV_ERR_UNTERM }
92 } else {
93 // Non-escaped field: runs until ',' CR LF.
94 cell.start = i
95 cell.quoted = 0
96 while i < n {
97 if buf[i] == 0x2C { break } // ','
98 if buf[i] == 0x0A { break } // LF
99 if buf[i] == 0x0D { break } // CR
100 i = i + 1
101 }
102 cell.end = i
103 }
104 cell_count = cell_count + 1
105
106 // After the cell we expect: comma, or newline, or EOF.
107 if i < n {
108 if buf[i] == 0x2C {
109 i = i + 1
110 continue
111 }
112 }
113 // End of row. Record row-boundary + advance past line break.
114 row_count = row_count + 1
115 if row_count >= rows_cap { return CSV_ERR_OVERFLOW }
116 rows[row_count] = cell_count
117
118 if i < n {
119 if buf[i] == 0x0D {
120 i = i + 1
121 }
122 }
123 if i < n {
124 if buf[i] == 0x0A {
125 i = i + 1
126 }
127 }
128 }
129 // If file didn't end with newline, cell_count > rows[row_count]
130 // means there's a trailing row we haven't committed.
131 if cell_count > 0 {
132 if row_count == 0 {
133 row_count = 1
134 if rows_cap > 1 { rows[1] = cell_count }
135 }
136 if rows[row_count] != cell_count {
137 row_count = row_count + 1
138 if row_count >= rows_cap { return CSV_ERR_OVERFLOW }
139 rows[row_count] = cell_count
140 }
141 }
142 return row_count
143}
144
145// Decode a quoted cell's "" escapes into a destination buffer.
146// Returns number of bytes written or CSV_ERR_OVERFLOW.
147func csv_decode_quoted(buf: *u8, start: i64, end: i64,
148 out: *u8, cap: i64) -> i64 {
149 var i: i64 = start
150 var w: i64 = 0
151 while i < end {
152 if buf[i] == 0x22 {
153 if i + 1 < end {
154 if buf[i + 1] == 0x22 {
155 if w >= cap { return CSV_ERR_OVERFLOW }
156 out[w] = 0x22
157 w = w + 1
158 i = i + 2
159 continue
160 }
161 }
162 }
163 if w >= cap { return CSV_ERR_OVERFLOW }
164 out[w] = buf[i]
165 w = w + 1
166 i = i + 1
167 }
168 return w
169}
170
171// Compile-only smoke.
172func main() -> i64 {
173 let cells_raw: *u8 = sys_mmap(CSV_MAGIC_1024)
174 let cells: *CsvCell = cells_raw as *CsvCell
175 let rows: *i64 = sys_mmap(128) as *i64
176
177 // "a,b,c\r\n1,\"hel,lo\",3\n"
178 let input: *u8 = "a,b,c\r\n1,\"hel,lo\",3\n"
179 let n_rows: i64 = csv_parse(input, 20, cells, 32, rows, 16)
180 if n_rows != 2 { return 1 }
181
182 // Row 0: [a, b, c] -- cells 0..2.
183 if rows[0] != 0 { return 2 }
184 if rows[1] != 3 { return 3 }
185 // Row 1: [1, "hel,lo", 3] -- cells 3..5.
186 if rows[2] != 6 { return 4 }
187
188 // Verify cell[4] is quoted and "hel,lo" content.
189 let c4: *CsvCell = cells + 4 * 24
190 if c4.quoted != 1 { return 5 }
191 if input[c4.start] != 0x68 { return 6 } // 'h'
192 if c4.end - c4.start != 6 { return 7 } // "hel,lo"
193
194 // Decode a quoted cell with "" escape: "he""llo"
195 let q: *u8 = "he\"\"llo" // raw bytes inside the quoted field
196 let tmp: *u8 = sys_mmap(32)
197 let wlen: i64 = csv_decode_quoted(q, 0, 7, tmp, 32)
198 if wlen != 6 { return 8 } // he"llo = 6 bytes
199 if tmp[2] != 0x22 { return 9 }
200 return 0
201}