nx_effect.nx source
↩ module page · 120 lines · 4053 B
1// effect.nx -- typed effects tracking (phase A: runtime log).
2//
3// EFFICIENCY_ROADMAP ยง3.4 -- algebraic effects / typed IO so
4// function signatures disclose their side effects. Pure-
5// function reasoning becomes possible; unexpected I/O from a
6// callee no longer hides.
7//
8// Phase A: a process-wide effect log. Functions that touch
9// FS/NET/ALLOC call `effect_mark(EFF_*)` at entry; unit tests
10// assert the log is empty after a computation that claims
11// purity. Cheap to adopt incrementally; no compiler changes
12// needed.
13//
14// Phase B (parser + typechecker): `can [FS, ALLOC]` row-type
15// on function signatures. Caller inherits the UNION of callee
16// effects automatically. Koka / Eff / OCaml-effects style.
17//
18// Phase C: handler syntax. `try { ... } with FS_open -> ... `
19// for mocking / redirection / sandboxing without globals.
20//
21// Invariants:
22// EF1 `effect_mark` is O(1) + non-allocating -- safe in
23// hot paths.
24// EF2 `effect_snapshot` captures the current effect set;
25// `effect_restore` rolls back. Enables nested purity
26// zones.
27// EF3 Effect constants are bitflags so multi-effect code
28// records a union with one OR.
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
34// verdict: NOT_YET_EVALUATED
35
36import "nx_syscalls.nx"
37
38const EFF_NONE: i64 = 0
39const EFF_FS: i64 = 0x0001 // filesystem
40const EFF_NET: i64 = 0x0002 // network
41const EFF_ALLOC: i64 = 0x0004 // dynamic memory
42const EFF_PROC: i64 = 0x0008 // fork/exec
43const EFF_CLOCK: i64 = 0x0010 // read wall-clock
44const EFF_RANDOM: i64 = 0x0020 // entropy source
45const EFF_IO: i64 = 0x0040 // stdin/stdout/stderr
46const EFF_SYSCALL: i64 = 0x0080 // raw syscall
47const EFF_UNWIND: i64 = 0x0100 // may panic/throw
48const EFF_NONDET: i64 = 0x0200 // nondeterministic result
49
50// Process-wide effect bitmask. Single i64 = no allocation + no
51// locking (single-threaded runtime assumption).
52struct EffectState {
53 bits: i64,
54}
55
56// Lazy singleton.
57func effect_state() -> *EffectState {
58 // We intentionally leak a mmap'd 16-byte block on first call.
59 // The state lives for the whole process -- no free path
60 // needed. Multi-thread-safe version goes in phase B.
61 let raw: *u8 = sys_mmap(16)
62 let s: *EffectState = raw as *EffectState
63 s.bits = 0
64 return s
65}
66
67// Mark that `eff` was observed. Call at the entry of every
68// function that touches that effect domain.
69func effect_mark(s: *EffectState, eff: i64) -> i64 {
70 s.bits = s.bits | eff
71 return 0
72}
73
74// Snapshot + restore for bounded purity zones.
75func effect_snapshot(s: *EffectState) -> i64 {
76 return s.bits
77}
78
79func effect_restore(s: *EffectState, saved: i64) -> i64 {
80 s.bits = saved
81 return 0
82}
83
84// Assert the state only contains effects from a given allow-list.
85// Returns 0 on OK, the unexpected bitmask on failure.
86func effect_assert(s: *EffectState, allowed: i64) -> i64 {
87 let leaked: i64 = s.bits & (allowed ^ (0 - 1))
88 if leaked != 0 { return leaked }
89 return 0
90}
91
92// Clear -- use between test cases so state doesn't leak.
93func effect_clear(s: *EffectState) -> i64 {
94 s.bits = 0
95 return 0
96}
97
98// Compile-only smoke.
99func main() -> i64 {
100 let s: *EffectState = effect_state()
101 if s.bits != 0 { return 1 }
102
103 // Log an FS + ALLOC effect.
104 effect_mark(s, EFF_FS)
105 effect_mark(s, EFF_ALLOC)
106 if s.bits != (EFF_FS | EFF_ALLOC) { return 2 }
107
108 // Assert we only leaked allowed effects.
109 if effect_assert(s, EFF_FS | EFF_ALLOC) != 0 { return 3 }
110 if effect_assert(s, EFF_FS) != EFF_ALLOC { return 4 }
111
112 // Snapshot / restore.
113 let save: i64 = effect_snapshot(s)
114 effect_mark(s, EFF_NET)
115 if (s.bits & EFF_NET) != EFF_NET { return 5 }
116 effect_restore(s, save)
117 if (s.bits & EFF_NET) != 0 { return 6 }
118
119 return 0
120}