code wiki / (root) / nx_ulid.nx

nx_ulid.nx source

↩ module page · 198 lines · 6987 B

1// ulid.nx -- Universally Unique Lexicographically Sortable ID. 2// 3// Alizain Feerasta 2016 spec. Drop-in UUID replacement designed 4// for databases + sortable indexes: 5// - 128 bits, same width as UUID 6// - First 48 bits: unix_ms timestamp (big-endian); sorts 7// chronologically in a SQL ORDER BY or filesystem listing 8// - Last 80 bits: cryptographic randomness 9// - Encoded as 26 Crockford-32 characters (no hyphens) 10// 11// Examples: 12// 01ARZ3NDEKTSV4RRFFQ69G5FAV 13// 01H7TNAC9TZ42D8XGTVJBKRS7M 14// 15// Used by: modern databases (PostgreSQL ULID extension, MongoDB 16// BSON_ULID), distributed systems needing time-ordered IDs, 17// log correlation, event sourcing, request tracing. 18// 19// Composes rand.nx (80 bits of randomness) + crockford32.nx 20// (canonical encoding). Time source is a caller-supplied 21// unix_ms -- keeps the module pure/testable. 22// 23// Invariants: 24// U1 Output = 26 ASCII chars, uppercase Crockford-32. 25// U2 First 10 chars encode 48-bit timestamp; lex-sort and 26// timestamp-sort agree within 2^48ms (~8900 years). 27// U3 Same millisecond -> different IDs (random suffix 28// differs); no strict monotonic-within-ms ordering 29// guaranteed today (that's a future ULID_MONOTONIC variant). 30 31// nx_safety_envelope: 32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 33// sil_target: SIL1 34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 35// verdict: NOT_YET_EVALUATED 36 37import "nx_syscalls.nx" 38import "nx_rand.nx" 39import "nx_crockford32.nx" 40const ULID_MAGIC_1777000000000: i64 = 1777000000000 41const ULID_MAGIC_1777000001000: i64 = 1777000001000 42 43const ULID_LEN: i64 = 26 44const ULID_BYTES: i64 = 16 45 46// Build a 16-byte ULID value into `out`. Caller handles time. 47func ulid_bytes(unix_ms: i64, out: *u8) -> i64 { 48 // 48-bit big-endian timestamp into out[0..6]. 49 out[0] = (unix_ms >> 40) & 0xFF 50 out[1] = (unix_ms >> 32) & 0xFF 51 out[2] = (unix_ms >> 24) & 0xFF 52 out[3] = (unix_ms >> 16) & 0xFF 53 out[4] = (unix_ms >> 8) & 0xFF 54 out[5] = unix_ms & 0xFF 55 // 80 random bits into out[6..16]. 56 rand_bytes(out + 6, 10) 57 return 0 58} 59 60// Encode a 16-byte ULID as 26 Crockford-32 chars. Specialised 61// here (not via c32_encode) because ULID's 128 bits don't align 62// to base-32 group boundaries; the layout is: 63// time high: first 2 chars (10 high bits of timestamp) 64// -- wait, actually ULID packs 128 bits into 26 chars by 65// padding 2 zero bits at the top. We encode the bitstream 66// big-end-first, 5 bits at a time. 67func ulid_render(bytes: *u8, out: *u8) -> i64 { 68 // Pack 16 bytes big-endian into a 130-bit virtual stream (with 69 // 2 leading zero bits), emit 26 5-bit groups. We produce each 70 // group by extracting the appropriate slice of the input byte 71 // array. 72 // 73 // The canonical ULID spec has specific alignment: 74 // char 0 = top 3 bits of byte 0 (prefixed with 2 zeros -> top bit 0) 75 // char 1 = bottom 5 bits of byte 0, top 0 bits of byte 1 76 // ... 77 // To stay simple we do a shift register: build a 128-bit 78 // number from bytes, emit 26 chars from the TOP 130 bits 79 // (treating the buffer as having 2 leading zero bits). 80 // 81 // NishiLang i64 is 64-bit so we handle the 128-bit value as 82 // two halves (hi, lo) of 64 bits each. Shift operations on 83 // the pair extract 5 bits at a time from the top. 84 let hi_raw: *i64 = (sys_mmap(16)) as *i64 85 let lo_raw: *i64 = (sys_mmap(16)) as *i64 86 var hi: i64 = 0 87 var lo: i64 = 0 88 var i: i64 = 0 89 while i < 8 { 90 hi = (hi << 8) | bytes[i] 91 i = i + 1 92 } 93 while i < 16 { 94 lo = (lo << 8) | bytes[i] 95 i = i + 1 96 } 97 98 // 130-bit stream has the 128 bits of (hi, lo) starting at 99 // bit index 2 (top two bits are the padding zeros). Emit 100 // 26 groups of 5 bits, top-first. 101 // 102 // For each group g in 0..25: 103 // bit_top_pos = 130 - 5*(g+1) (from LSB side of the 130-bit value) 104 // 105 // Use a helper to extract 5 bits at a given LSB position. 106 var g: i64 = 0 107 while g < 26 { 108 let bit_pos: i64 = 125 - 5 * g // LSB of this 5-bit group 109 var v: i64 = 0 110 if bit_pos >= 64 { 111 // All 5 bits live in hi. 112 v = (hi >> (bit_pos - 64)) & 0x1F 113 } else { 114 if bit_pos + 5 <= 64 { 115 // All 5 bits live in lo. 116 v = (lo >> bit_pos) & 0x1F 117 } else { 118 // Straddles: low part from lo, high part from hi. 119 let lo_bits: i64 = 64 - bit_pos // how many from lo 120 let lo_mask: i64 = (1 << lo_bits) - 1 121 let lo_part: i64 = (lo >> bit_pos) & lo_mask 122 let hi_part: i64 = hi & ((1 << (5 - lo_bits)) - 1) 123 v = (hi_part << lo_bits) | lo_part 124 } 125 } 126 out[g] = c32_enc_char(v) 127 g = g + 1 128 } 129 *hi_raw = hi // discard-use so the vars don't get optimised to unused 130 *lo_raw = lo 131 return ULID_LEN 132} 133 134// Convenience: emit a fresh 26-char ULID at caller's time. 135func ulid_new(unix_ms: i64, out: *u8) -> i64 { 136 let tmp: *u8 = sys_mmap(ULID_BYTES) 137 ulid_bytes(unix_ms, tmp) 138 return ulid_render(tmp, out) 139} 140 141// Compile-only smoke. 142func main() -> i64 { 143 let out: *u8 = sys_mmap(64) 144 ulid_new(ULID_MAGIC_1777000000000, out) 145 // All 26 characters must be valid Crockford-32 (alnum minus 146 // I/L/O/U; encoder always upper-case). 147 var i: i64 = 0 148 while i < 26 { 149 let c: i64 = out[i] 150 var ok: i64 = 0 151 if c >= 0x30 { 152 if c <= 0x39 { ok = 1 } 153 } 154 if c >= 0x41 { 155 if c <= 0x5A { 156 if c != 0x49 { // 'I' 157 if c != 0x4C { // 'L' 158 if c != 0x4F { // 'O' 159 if c != 0x55 { // 'U' 160 ok = 1 161 } 162 } 163 } 164 } 165 } 166 } 167 if ok == 0 { return 1 } 168 i = i + 1 169 } 170 171 // Two ULIDs at the same timestamp should differ (random suffix). 172 let out2: *u8 = sys_mmap(64) 173 ulid_new(ULID_MAGIC_1777000000000, out2) 174 var diff: i64 = 0 175 i = 0 176 while i < 26 { 177 if out[i] != out2[i] { diff = 1; break } 178 i = i + 1 179 } 180 if diff != 1 { return 2 } 181 182 // Later timestamp should produce a lexicographically-greater 183 // prefix (first 10 chars). This is the whole reason ULID exists. 184 let out3: *u8 = sys_mmap(64) 185 ulid_new(ULID_MAGIC_1777000001000, out3) 186 // Compare first 10 chars. Can't be lower -- at minimum 187 // later timestamps give equal-or-greater prefix. 188 var worse: i64 = 0 189 i = 0 190 while i < 10 { 191 if out3[i] < out[i] { worse = 1; break } 192 if out3[i] > out[i] { break } 193 i = i + 1 194 } 195 if worse == 1 { return 3 } 196 197 return 0 198}