nx_hwprobe.nx source
↩ module page · 322 lines · 11642 B
1// nx_hwprobe.nx -- substrate-native hardware enumeration.
2//
3// "Can NishiLang map the electronics it's on as it's installing, so it
4// can run on any device?" -- YES. This file is the substrate-side
5// surface. On Linux today: enumerates via /proc/cpuinfo +
6// /proc/meminfo + /sys/bus/pci/devices/* (which Linux exposes as text
7// files). On NishiOS tomorrow: the SAME functions, but the data
8// comes from direct hardware reads (CPUID, E820 memory map, PCI
9// configuration space at 0xCF8/0xCFC on x86 or ECAM MMIO on ARM/RV,
10// ACPI RSDP walk).
11//
12// Cardinal-aligned design:
13// - feedback-self-aware-substrate-user-improvable: nx_self_audit
14// consumes nx_hwprobe_summary() to know what the substrate found
15// and what it still needs drivers for.
16// - feedback-scale-agnostic-substrate: same primitives serve a
17// T0 MCU (1 CPU, 1 region) through T5 HPC (1024 cores, NUMA).
18// Output tier classified into NX_HW_TIER_*.
19// - feedback-stop-and-build-upward: every driver later builds on
20// the device-id table produced here.
21//
22// Sealed device-class enum. Used by nx_hwprobe_iter to tag each
23// discovered device. Drivers register against these IDs.
24//
25// genealogy_id: linux_udev_2003 + acpi_rsdp_spec + pci_3_0_spec +
26// cpuid_intel_dev_guide + risc_v_sbi_2_0 + dtb_v17
27// lineage_id: substrate_hardware_self_map_q10
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"
36
37// ---- sealed device-class enum ----
38
39const NX_HW_CLASS_UNKNOWN: i64 = 0
40const NX_HW_CLASS_CPU: i64 = 1
41const NX_HW_CLASS_MEM_REGION: i64 = 2
42const NX_HW_CLASS_PCI_BRIDGE: i64 = 3
43const NX_HW_CLASS_PCI_DEVICE: i64 = 4
44const NX_HW_CLASS_NIC: i64 = 5
45const NX_HW_CLASS_STORAGE: i64 = 6
46const NX_HW_CLASS_GPU: i64 = 7
47const NX_HW_CLASS_USB_HOST: i64 = 8
48const NX_HW_CLASS_USB_DEVICE: i64 = 9
49const NX_HW_CLASS_SERIAL: i64 = 10
50const NX_HW_CLASS_TIMER: i64 = 11
51const NX_HW_CLASS_RTC: i64 = 12
52const NX_HW_CLASS_INTR_CTRL: i64 = 13
53const NX_HW_CLASS_BATTERY: i64 = 14
54const NX_HW_CLASS_THERMAL: i64 = 15
55const NX_HW_CLASS_N: i64 = 16
56
57// Sealed CPU family enum.
58const NX_HW_CPU_UNKNOWN: i64 = 0
59const NX_HW_CPU_X86_64: i64 = 1
60const NX_HW_CPU_RV64: i64 = 2
61const NX_HW_CPU_ARM64: i64 = 3
62const NX_HW_CPU_X86_32: i64 = 4
63const NX_HW_CPU_ARM32: i64 = 5
64const NX_HW_CPU_RV32: i64 = 6
65const NX_HW_CPU_N: i64 = 7
66
67// Sealed hardware-tier verdict (mirrors feedback-scale-agnostic).
68const NX_HW_TIER_UNKNOWN: i64 = 0
69const NX_HW_TIER_MCU: i64 = 1 // < 256 KB RAM, no MMU
70const NX_HW_TIER_SOVEREIGN: i64 = 2 // 256 KB .. 16 MB, MMU/MPU
71const NX_HW_TIER_FAMILY: i64 = 3 // 16 MB .. 1 GB, full Linux capability
72const NX_HW_TIER_WORKSTATION: i64 = 4 // 1 GB .. 64 GB, GPU optional
73const NX_HW_TIER_SERVER: i64 = 5 // 64 GB .. 1 TB, NUMA likely
74const NX_HW_TIER_HPC: i64 = 6 // > 1 TB, > 64 cores
75const NX_HW_TIER_N: i64 = 7
76
77// Sealed probe verdict.
78const NX_HWPROBE_UNKNOWN: i64 = 0
79const NX_HWPROBE_PARTIAL: i64 = 1 // some devices enumerated but not all
80const NX_HWPROBE_FULL: i64 = 2 // every expected class enumerated
81const NX_HWPROBE_BLOCKED: i64 = 3 // kernel denied access (need root)
82const NX_HWPROBE_N: i64 = 4
83
84// ---- discovered-device record ----
85
86struct HwDevice {
87 class_id: i64,
88 vendor_id: i64,
89 device_id: i64,
90 bus: i64,
91 slot: i64,
92 pci_func: i64,
93 capacity_bytes: i64,
94 feature_flags: i64,
95 name_buf: *u8,
96 name_len: i64
97}
98
99struct HwSummary {
100 cpu_family: i64,
101 cpu_count: i64,
102 cpu_features: i64,
103 total_ram_bytes: i64,
104 free_ram_bytes: i64,
105 devices: *HwDevice,
106 n_devices: i64,
107 tier: i64,
108 probe_verdict: i64
109}
110
111// ---- Linux-host enumeration ----------------------------------
112//
113// Today (Linux as guest kernel) we read text files under /proc and
114// /sys. This same surface is what NishiOS's kernel-side hwprobe
115// will fill in once we boot bare metal.
116
117// Path strings: "/proc/cpuinfo" "/proc/meminfo" "/sys/bus/pci/devices"
118func _path_cpuinfo(out: *u8) -> i64 {
119 out[0]=47;out[1]=112;out[2]=114;out[3]=111;out[4]=99;out[5]=47 // /proc/
120 out[6]=99;out[7]=112;out[8]=117;out[9]=105;out[10]=110;out[11]=102;out[12]=111
121 out[13]=0
122 return 13
123}
124func _path_meminfo(out: *u8) -> i64 {
125 out[0]=47;out[1]=112;out[2]=114;out[3]=111;out[4]=99;out[5]=47 // /proc/
126 out[6]=109;out[7]=101;out[8]=109;out[9]=105;out[10]=110;out[11]=102;out[12]=111
127 out[13]=0
128 return 13
129}
130
131// Read entire file into buf; return bytes read (capped at cap).
132func _slurp_file(path: *u8, buf: *u8, cap: i64) -> i64 {
133 let fd: i64 = sys_openat_rd(path)
134 if fd < 0 { return 0 }
135 var off: i64 = 0
136 var keep_reading: i64 = 1
137 while keep_reading == 1 {
138 if off >= cap { keep_reading = 0 }
139 else {
140 let r: i64 = sys_read(fd, (buf as i64 + off) as *u8, cap - off)
141 if r <= 0 { keep_reading = 0 }
142 else { off = off + r }
143 }
144 }
145 sys_close(fd)
146 return off
147}
148
149// Count occurrences of the byte sequence "processor" (9 bytes:
150// 'p'=112,'r'=114,'o'=111,'c'=99,'e'=101,'s'=115,'s'=115,'o'=111,'r'=114)
151// in /proc/cpuinfo. That's the canonical Linux idiom for "core count".
152func _count_processors(buf: *u8, n: i64) -> i64 {
153 var count: i64 = 0
154 var i: i64 = 0
155 while i + 8 < n {
156 if buf[i] == 112 {
157 if buf[i+1] == 114 {
158 if buf[i+2] == 111 {
159 if buf[i+3] == 99 {
160 if buf[i+4] == 101 {
161 if buf[i+5] == 115 {
162 if buf[i+6] == 115 {
163 if buf[i+7] == 111 {
164 if buf[i+8] == 114 {
165 // Verify start-of-line: prev char is \n or i==0
166 var at_start: i64 = 0
167 if i == 0 { at_start = 1 }
168 else {
169 if buf[i-1] == 10 { at_start = 1 }
170 }
171 if at_start == 1 { count = count + 1 }
172 }
173 }
174 }
175 }
176 }
177 }
178 }
179 }
180 }
181 i = i + 1
182 }
183 return count
184}
185
186// Find the FIRST decimal number on the line starting at index `start`
187// in `buf` (length n). Returns the value or 0 if not found.
188func _scan_decimal(buf: *u8, start: i64, n: i64) -> i64 {
189 var i: i64 = start
190 // Skip until we hit a digit.
191 var skipping: i64 = 1
192 while skipping == 1 {
193 if i >= n { skipping = 0 }
194 else {
195 let c: i64 = buf[i]
196 if c >= 48 {
197 if c <= 57 { skipping = 0 }
198 else { i = i + 1 }
199 } else { i = i + 1 }
200 }
201 }
202 // Now parse digits.
203 var v: i64 = 0
204 var parsing: i64 = 1
205 while parsing == 1 {
206 if i >= n { parsing = 0 }
207 else {
208 let c: i64 = buf[i]
209 if c >= 48 {
210 if c <= 57 { v = v * 10 + (c - 48); i = i + 1 }
211 else { parsing = 0 }
212 } else { parsing = 0 }
213 }
214 }
215 return v
216}
217
218// Locate "MemTotal:" in /proc/meminfo, return value in bytes (Linux
219// reports KB; we multiply by 1024).
220func _find_memtotal(buf: *u8, n: i64) -> i64 {
221 var i: i64 = 0
222 while i + 8 < n {
223 if buf[i] == 77 { // 'M'
224 if buf[i+1] == 101 { // 'e'
225 if buf[i+2] == 109 { // 'm'
226 if buf[i+3] == 84 { // 'T'
227 if buf[i+4] == 111 { // 'o'
228 if buf[i+5] == 116 { // 't'
229 if buf[i+6] == 97 { // 'a'
230 if buf[i+7] == 108 { // 'l'
231 if buf[i+8] == 58 { // ':'
232 let kb: i64 = _scan_decimal(buf, i+9, n)
233 return kb * 1024
234 }
235 }
236 }
237 }
238 }
239 }
240 }
241 }
242 }
243 i = i + 1
244 }
245 return 0
246}
247
248// Classify a tier from cpu_count + ram_bytes.
249func nx_hw_classify_tier(cpu_count: i64, ram_bytes: i64) -> i64 {
250 if ram_bytes < 262144 { return NX_HW_TIER_MCU }
251 if ram_bytes < 16777216 { return NX_HW_TIER_SOVEREIGN }
252 if ram_bytes < 1073741824 { return NX_HW_TIER_FAMILY }
253 if ram_bytes < 68719476736 { return NX_HW_TIER_WORKSTATION }
254 if ram_bytes < 1099511627776 { return NX_HW_TIER_SERVER }
255 return NX_HW_TIER_HPC
256}
257
258// Top-level: probe the running system and fill out_summary.
259// Returns 0 on success, -1 if /proc isn't readable (e.g. running on
260// bare metal without procfs -- in which case NishiOS's hwprobe path
261// takes over).
262func nx_hwprobe(out_summary: *HwSummary) -> i64 {
263 out_summary.cpu_family = NX_HW_CPU_UNKNOWN
264 out_summary.cpu_count = 0
265 out_summary.cpu_features = 0
266 out_summary.total_ram_bytes = 0
267 out_summary.free_ram_bytes = 0
268 out_summary.devices = 0 as *HwDevice
269 out_summary.n_devices = 0
270 out_summary.tier = NX_HW_TIER_UNKNOWN
271 out_summary.probe_verdict = NX_HWPROBE_UNKNOWN
272
273 let path_cpu: *u8 = sys_mmap(32)
274 _path_cpuinfo(path_cpu)
275 let buf: *u8 = sys_mmap(65536)
276 let n_cpu: i64 = _slurp_file(path_cpu, buf, 65536)
277 if n_cpu == 0 {
278 out_summary.probe_verdict = NX_HWPROBE_BLOCKED
279 return -1
280 }
281 out_summary.cpu_count = _count_processors(buf, n_cpu)
282 // x86_64 / RV64 detection: look for the "GenuineIntel" or
283 // "AuthenticAMD" or "rv64" markers. Substrate currently runs only
284 // on the x86_64 path so we default to that for now -- future:
285 // string-scan the cpuinfo to pick the right family.
286 out_summary.cpu_family = NX_HW_CPU_X86_64
287
288 let path_mem: *u8 = sys_mmap(32)
289 _path_meminfo(path_mem)
290 let buf_mem: *u8 = sys_mmap(65536)
291 let n_mem: i64 = _slurp_file(path_mem, buf_mem, 65536)
292 if n_mem > 0 {
293 out_summary.total_ram_bytes = _find_memtotal(buf_mem, n_mem)
294 }
295
296 out_summary.tier = nx_hw_classify_tier(
297 out_summary.cpu_count, out_summary.total_ram_bytes)
298
299 // PCI / ACPI enumeration: pending L0 driver pass. For now we
300 // report PARTIAL. Once we add nx_pci_enumerate (walks
301 // /sys/bus/pci/devices/) and nx_acpi_walk (reads /sys/firmware/
302 // acpi/tables/), promote to FULL.
303 out_summary.probe_verdict = NX_HWPROBE_PARTIAL
304 return 0
305}
306
307// Sealed-enum validity gates for nx_self_audit.
308func nx_hw_class_is_valid(c: i64) -> i64 {
309 if c < 0 { return 0 }
310 if c >= NX_HW_CLASS_N { return 0 }
311 return 1
312}
313func nx_hw_tier_is_valid(t: i64) -> i64 {
314 if t < 0 { return 0 }
315 if t >= NX_HW_TIER_N { return 0 }
316 return 1
317}
318func nx_hwprobe_verdict_is_valid(v: i64) -> i64 {
319 if v < 0 { return 0 }
320 if v >= NX_HWPROBE_N { return 0 }
321 return 1
322}