code wiki / (root) / ulid.nx

ulid.nx source

↩ module page · 190 lines · 6780 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 31import "syscalls.nx" 32import "rand.nx" 33import "crockford32.nx" 34 35const ULID_LEN: i64 = 26 36const ULID_BYTES: i64 = 16 37 38// Build a 16-byte ULID value into `out`. Caller handles time. 39func ulid_bytes(unix_ms: i64, out: *u8) -> i64 { 40 // 48-bit big-endian timestamp into out[0..6]. 41 out[0] = (unix_ms >> 40) & 0xFF 42 out[1] = (unix_ms >> 32) & 0xFF 43 out[2] = (unix_ms >> 24) & 0xFF 44 out[3] = (unix_ms >> 16) & 0xFF 45 out[4] = (unix_ms >> 8) & 0xFF 46 out[5] = unix_ms & 0xFF 47 // 80 random bits into out[6..16]. 48 rand_bytes(out + 6, 10) 49 return 0 50} 51 52// Encode a 16-byte ULID as 26 Crockford-32 chars. Specialised 53// here (not via c32_encode) because ULID's 128 bits don't align 54// to base-32 group boundaries; the layout is: 55// time high: first 2 chars (10 high bits of timestamp) 56// -- wait, actually ULID packs 128 bits into 26 chars by 57// padding 2 zero bits at the top. We encode the bitstream 58// big-end-first, 5 bits at a time. 59func ulid_render(bytes: *u8, out: *u8) -> i64 { 60 // Pack 16 bytes big-endian into a 130-bit virtual stream (with 61 // 2 leading zero bits), emit 26 5-bit groups. We produce each 62 // group by extracting the appropriate slice of the input byte 63 // array. 64 // 65 // The canonical ULID spec has specific alignment: 66 // char 0 = top 3 bits of byte 0 (prefixed with 2 zeros -> top bit 0) 67 // char 1 = bottom 5 bits of byte 0, top 0 bits of byte 1 68 // ... 69 // To stay simple we do a shift register: build a 128-bit 70 // number from bytes, emit 26 chars from the TOP 130 bits 71 // (treating the buffer as having 2 leading zero bits). 72 // 73 // NishiLang i64 is 64-bit so we handle the 128-bit value as 74 // two halves (hi, lo) of 64 bits each. Shift operations on 75 // the pair extract 5 bits at a time from the top. 76 let hi_raw: *i64 = (sys_mmap(16)) as *i64 77 let lo_raw: *i64 = (sys_mmap(16)) as *i64 78 var hi: i64 = 0 79 var lo: i64 = 0 80 var i: i64 = 0 81 while i < 8 { 82 hi = (hi << 8) | bytes[i] 83 i = i + 1 84 } 85 while i < 16 { 86 lo = (lo << 8) | bytes[i] 87 i = i + 1 88 } 89 90 // 130-bit stream has the 128 bits of (hi, lo) starting at 91 // bit index 2 (top two bits are the padding zeros). Emit 92 // 26 groups of 5 bits, top-first. 93 // 94 // For each group g in 0..25: 95 // bit_top_pos = 130 - 5*(g+1) (from LSB side of the 130-bit value) 96 // 97 // Use a helper to extract 5 bits at a given LSB position. 98 var g: i64 = 0 99 while g < 26 { 100 let bit_pos: i64 = 125 - 5 * g // LSB of this 5-bit group 101 var v: i64 = 0 102 if bit_pos >= 64 { 103 // All 5 bits live in hi. 104 v = (hi >> (bit_pos - 64)) & 0x1F 105 } else { 106 if bit_pos + 5 <= 64 { 107 // All 5 bits live in lo. 108 v = (lo >> bit_pos) & 0x1F 109 } else { 110 // Straddles: low part from lo, high part from hi. 111 let lo_bits: i64 = 64 - bit_pos // how many from lo 112 let lo_mask: i64 = (1 << lo_bits) - 1 113 let lo_part: i64 = (lo >> bit_pos) & lo_mask 114 let hi_part: i64 = hi & ((1 << (5 - lo_bits)) - 1) 115 v = (hi_part << lo_bits) | lo_part 116 } 117 } 118 out[g] = c32_enc_char(v) 119 g = g + 1 120 } 121 *hi_raw = hi // discard-use so the vars don't get optimised to unused 122 *lo_raw = lo 123 return ULID_LEN 124} 125 126// Convenience: emit a fresh 26-char ULID at caller's time. 127func ulid_new(unix_ms: i64, out: *u8) -> i64 { 128 let tmp: *u8 = sys_mmap(ULID_BYTES) 129 ulid_bytes(unix_ms, tmp) 130 return ulid_render(tmp, out) 131} 132 133// Compile-only smoke. 134func main() -> i64 { 135 let out: *u8 = sys_mmap(64) 136 ulid_new(1777000000000, out) 137 // All 26 characters must be valid Crockford-32 (alnum minus 138 // I/L/O/U; encoder always upper-case). 139 var i: i64 = 0 140 while i < 26 { 141 let c: i64 = out[i] 142 var ok: i64 = 0 143 if c >= 0x30 { 144 if c <= 0x39 { ok = 1 } 145 } 146 if c >= 0x41 { 147 if c <= 0x5A { 148 if c != 0x49 { // 'I' 149 if c != 0x4C { // 'L' 150 if c != 0x4F { // 'O' 151 if c != 0x55 { // 'U' 152 ok = 1 153 } 154 } 155 } 156 } 157 } 158 } 159 if ok == 0 { return 1 } 160 i = i + 1 161 } 162 163 // Two ULIDs at the same timestamp should differ (random suffix). 164 let out2: *u8 = sys_mmap(64) 165 ulid_new(1777000000000, out2) 166 var diff: i64 = 0 167 i = 0 168 while i < 26 { 169 if out[i] != out2[i] { diff = 1; break } 170 i = i + 1 171 } 172 if diff != 1 { return 2 } 173 174 // Later timestamp should produce a lexicographically-greater 175 // prefix (first 10 chars). This is the whole reason ULID exists. 176 let out3: *u8 = sys_mmap(64) 177 ulid_new(1777000001000, out3) 178 // Compare first 10 chars. Can't be lower -- at minimum 179 // later timestamps give equal-or-greater prefix. 180 var worse: i64 = 0 181 i = 0 182 while i < 10 { 183 if out3[i] < out[i] { worse = 1; break } 184 if out3[i] > out[i] { break } 185 i = i + 1 186 } 187 if worse == 1 { return 3 } 188 189 return 0 190}