nx_io.nx source
↩ module page · 172 lines · 5107 B
1// nx_io.nx -- buffered fd I/O (line-buffered stdout, full-buffered file).
2//
3// Today every nx_*.nx tool uses raw `sys_write(fd, buf, n)`. For
4// per-character or per-token output (disassembler, hex dumps, log
5// formatters) that's a syscall per byte -- thousands of context
6// switches for one tool invocation.
7//
8// This module wraps each fd in a 4 KiB buffer:
9// io = nx_io_open_fd(fd, mode)
10// nx_io_write(io, "hello", 5)
11// nx_io_putc(io, 0x21)
12// nx_io_flush(io)
13// nx_io_close(io)
14//
15// modes:
16// NX_IO_LINEBUF : flush on '\n' (good for stderr / interactive)
17// NX_IO_FULLBUF : flush only when buffer full or explicit flush
18//
19// Pairs with nx_strfmt (formatter takes an fd today, can take an
20// nx_io once we wire it through) + nx_log + nx_dbg.
21
22// nx_safety_envelope:
23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
24// sil_target: SIL1
25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
26// verdict: NOT_YET_EVALUATED
27
28import "syscalls.nx"
29
30const NX_IO_BUFSZ: i64 = 4096
31const NX_IO_LINEBUF: i64 = 1
32const NX_IO_FULLBUF: i64 = 2
33
34struct NxIoFd {
35 fd: i64,
36 buf: *u8,
37 pos: i64,
38 cap: i64,
39 mode: i64,
40}
41
42const NX_IO_FD_BYTES: i64 = 40
43
44func nx_io_open_fd(fd: i64, mode: i64) -> *NxIoFd {
45 let raw: *u8 = sys_mmap(NX_IO_FD_BYTES)
46 let io: *NxIoFd = raw as *NxIoFd
47 io.fd = fd
48 io.buf = sys_mmap(NX_IO_BUFSZ)
49 io.pos = 0
50 io.cap = NX_IO_BUFSZ
51 io.mode = mode
52 return io
53}
54
55// Drain the buffer to the underlying fd. Returns 0 on success or
56// the negative error from sys_write.
57func nx_io_flush(io: *NxIoFd) -> i64 {
58 if io.pos == 0 { return 0 }
59 let n: i64 = sys_write(io.fd, io.buf, io.pos)
60 if n < 0 { return n }
61 io.pos = 0
62 return 0
63}
64
65// Append one byte; flush if buffer is full or (LINEBUF + byte = '\n').
66func nx_io_putc(io: *NxIoFd, c: i64) -> i64 {
67 if io.pos >= io.cap {
68 let r: i64 = nx_io_flush(io)
69 if r < 0 { return r }
70 }
71 io.buf[io.pos] = c & 0xFF
72 io.pos = io.pos + 1
73 if io.mode == NX_IO_LINEBUF {
74 if c == 0x0A { return nx_io_flush(io) }
75 }
76 return 0
77}
78
79// Append `n` bytes from `src`. Splits writes that would overflow
80// the buffer into a flush + tail-copy.
81func nx_io_write(io: *NxIoFd, src: *u8, n: i64) -> i64 {
82 var i: i64 = 0
83 while i < n {
84 let space: i64 = io.cap - io.pos
85 if space <= 0 {
86 let r: i64 = nx_io_flush(io)
87 if r < 0 { return r }
88 } else {
89 var chunk: i64 = n - i
90 if chunk > space { chunk = space }
91 var k: i64 = 0
92 while k < chunk {
93 io.buf[io.pos + k] = src[i + k]
94 k = k + 1
95 }
96 io.pos = io.pos + chunk
97 i = i + chunk
98 if io.mode == NX_IO_LINEBUF {
99 // If the chunk we just wrote contained a '\n',
100 // flush. Cheaper than scanning every byte: just
101 // check the new tail.
102 var found: i64 = 0
103 var s: i64 = io.pos - chunk
104 while s < io.pos {
105 if io.buf[s] == 0x0A { found = 1; s = io.pos }
106 else { s = s + 1 }
107 }
108 if found == 1 {
109 let rf: i64 = nx_io_flush(io)
110 if rf < 0 { return rf }
111 }
112 }
113 }
114 }
115 return 0
116}
117
118// Append a NUL-terminated string.
119func nx_io_puts(io: *NxIoFd, s: *u8) -> i64 {
120 var n: i64 = 0
121 while s[n] != 0 { n = n + 1 }
122 return nx_io_write(io, s, n)
123}
124
125// Flush + drop the wrapper. Caller is responsible for closing the
126// underlying fd if they want it closed.
127func nx_io_close(io: *NxIoFd) -> i64 {
128 return nx_io_flush(io)
129}
130
131// ---- self-test ---------------------------------------------------
132
133func main() -> i64 {
134 let io: *NxIoFd = nx_io_open_fd(2, NX_IO_FULLBUF)
135
136 // Write 10 small chunks; nothing should reach fd until flush.
137 var i: i64 = 0
138 while i < 10 {
139 nx_io_putc(io, 0x2E)
140 i = i + 1
141 }
142 if io.pos != 10 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
143
144 nx_io_flush(io)
145 if io.pos != 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
146
147 // Big write: should auto-flush partway.
148 let big: *u8 = sys_mmap(NX_IO_BUFSZ + 256)
149 var k: i64 = 0
150 while k < NX_IO_BUFSZ + 256 {
151 big[k] = 0x20
152 k = k + 1
153 }
154 nx_io_write(io, big, NX_IO_BUFSZ + 256)
155 // After auto-flush, the residual should be exactly 256 bytes.
156 if io.pos != 256 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
157 nx_io_flush(io)
158
159 // Linebuf mode: '\n' triggers flush.
160 let io2: *NxIoFd = nx_io_open_fd(2, NX_IO_LINEBUF)
161 nx_io_putc(io2, 0x41) // 'A'
162 nx_io_putc(io2, 0x0A) // '\n' -> auto flush
163 if io2.pos != 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
164
165 // puts smoke
166 let s: *u8 = sys_mmap(8)
167 s[0] = 0x68; s[1] = 0x69; s[2] = 0x0A; s[3] = 0
168 nx_io_puts(io2, s)
169 if io2.pos != 0 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
170
171 return 0
172}