code wiki / (root) / nx_csv_v1.nx

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}