feat(stdlib): finish Crypto (CSPRNG + base64) and add Uuid.* library (#19 #16)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Crypto (#19): add the OS cryptographically-secure random surface (random_bytes/random_hex/random_u32, reading /dev/urandom) and a standard base64 encoder, completing the library alongside the existing SHA-256/ HMAC-SHA256/verify_hmac/hex/ct_equal. All pure integer IR, C-free. Uuid (#16): a new namespace for stable, collision-free IDs — v4 (random) and v7 (time-ordered) generation, plus parse/is_valid/to_text/equals/nil. UUIDs are canonical lowercase 36-char strings; v4 and v7's random tail draw from the crypto CSPRNG, so both carry the documented determinism caveat (mint at the edges, never inside lockstep simulation). Reuses the crypto prelude's fn_secure_bytes / fn_hex_encode. - examples/library/{crypto,uuid}.ludic: known-answer vectors (SHA-256, HMAC, base64 per RFC 4231/4648) and structural invariants (uuid version/variant bits, parse/equals), wired into `x test` (now 51 passed). - docs: per-symbol pages for every new method + a new Uuid section; inventory and impl-vs-docs coverage check pass. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a422ef4375
commit
2ddf830f0b
26 changed files with 12185 additions and 10720 deletions
|
|
@ -8,4 +8,6 @@ Secure, test-vector-backed hashing for the handful of security-sensitive things
|
|||
|
||||
Digests are returned as lowercase hex strings, not raw bytes — a <code>str</code> is null-terminated and a raw digest can contain a zero byte, so hex is the form you can print, store, and compare directly.
|
||||
|
||||
Alongside hashing, this library exposes the OS cryptographically-secure random generator — <a href="crypto-random_bytes"><code>random_bytes</code></a>, <a href="crypto-random_hex"><code>random_hex</code></a>, and <a href="crypto-random_u32"><code>random_u32</code></a> — for tokens, nonces, and <a href="ns-Uuid"><code>Uuid</code></a> generation, plus <a href="crypto-base64"><code>base64</code></a> for moving bytes through text-only channels. The secure-random helpers are deliberately non-deterministic and must never seed the lockstep simulation RNG (<a href="ns-Random"><code>Random</code></a>).
|
||||
|
||||
What this is not: it is not DRM and it is not unbeatable anti-cheat. A client-side game cannot keep a secret from the machine it runs on — a determined owner can always read the key out of the binary. Use it to make *casual* tampering detectable and to authenticate messages between parties who share a key. To verify a MAC always use <a href="crypto-verify_hmac"><code>Crypto.verify_hmac</code></a> (a constant-time check), never <code>==</code>, which leaks how much of a guessed MAC was correct.
|
||||
|
|
|
|||
28
docs/language/crypto/crypto-base64.md
Normal file
28
docs/language/crypto/crypto-base64.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: crypto-base64
|
||||
name: Crypto.base64
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.base64
|
||||
sig: Crypto.base64(s) -> string
|
||||
tip: Standard base64 (RFC 4648) of a string's bytes.
|
||||
order: 9
|
||||
ns: Crypto
|
||||
member: base64
|
||||
---
|
||||
|
||||
Encodes the bytes of <code>s</code> as standard base64 (RFC 4648, the <code>A–Z a–z 0–9 + /</code> alphabet with <code>=</code> padding). Base64 turns arbitrary bytes into printable ASCII, which is what you want when a digest, key, or binary blob has to travel through a text-only channel — a JSON field, a URL-safe token wrapper, a config file, a log line. It is an *encoding*, not encryption: it hides nothing and adds no integrity. Pair it with <a href="crypto-hmac_sha256"><code>Crypto.hmac_sha256</code></a> when the payload must also be tamper-evident.
|
||||
|
||||
The output length is always a multiple of four; a one- or two-byte remainder in the input is padded with <code>=</code>.
|
||||
|
||||
Parameters:
|
||||
- `s` — the string whose bytes are encoded
|
||||
|
||||
```ludic
|
||||
program Encode {
|
||||
entry {
|
||||
print(Crypto.base64("foobar")) # Zm9vYmFy
|
||||
print(Crypto.base64("f")) # Zg==
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/crypto/crypto-random_bytes.md
Normal file
28
docs/language/crypto/crypto-random_bytes.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: crypto-random_bytes
|
||||
name: Crypto.random_bytes
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.random_bytes
|
||||
sig: Crypto.random_bytes(n) -> string
|
||||
tip: n bytes from the OS CSPRNG, as a 2n-character hex string.
|
||||
order: 6
|
||||
ns: Crypto
|
||||
member: random_bytes
|
||||
---
|
||||
|
||||
Draws <code>n</code> bytes from the operating system's cryptographically-secure random number generator and returns them as a <code>2n</code>-character lowercase hex string. Use it for unguessable tokens, nonces, and session secrets — anything whose whole value is that an attacker cannot predict it. The result is hex rather than raw bytes for the same reason digests are: a <code>str</code> is null-terminated and raw random bytes can contain a zero byte, so hex is the form you can safely store and compare.
|
||||
|
||||
This is deliberately **non-deterministic** — it must never seed the lockstep simulation RNG (<a href="ns-Random"><code>Random</code></a>). Two calls return different values. On a platform without an OS CSPRNG (for example a bare wasm target) the draw degrades to zeroes rather than faulting; treat a real CSPRNG there as a platform-layer responsibility.
|
||||
|
||||
Parameters:
|
||||
- `n` — the number of secure random bytes to draw
|
||||
|
||||
```ludic
|
||||
program Token {
|
||||
entry {
|
||||
let session = Crypto.random_bytes(16) # 32 hex chars, unguessable
|
||||
print(len(session)) # 32
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/crypto/crypto-random_hex.md
Normal file
26
docs/language/crypto/crypto-random_hex.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: crypto-random_hex
|
||||
name: Crypto.random_hex
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.random_hex
|
||||
sig: Crypto.random_hex(n) -> string
|
||||
tip: Alias for random_bytes — n secure bytes as a 2n-char hex string.
|
||||
order: 7
|
||||
ns: Crypto
|
||||
member: random_hex
|
||||
---
|
||||
|
||||
Identical to <a href="crypto-random_bytes"><code>Crypto.random_bytes</code></a>: draws <code>n</code> bytes from the OS CSPRNG and returns them as a <code>2n</code>-character lowercase hex string. The two names are interchangeable; <code>random_hex</code> exists so call sites can be explicit that the return value is already hex text (not raw bytes) when that reads more clearly. The same determinism caveat applies — the result is unpredictable by design and must stay out of the reproducible simulation RNG.
|
||||
|
||||
Parameters:
|
||||
- `n` — the number of secure random bytes to draw
|
||||
|
||||
```ludic
|
||||
program Nonce {
|
||||
entry {
|
||||
let nonce = Crypto.random_hex(12) # 24 hex chars
|
||||
print(len(nonce)) # 24
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/crypto/crypto-random_u32.md
Normal file
26
docs/language/crypto/crypto-random_u32.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: crypto-random_u32
|
||||
name: Crypto.random_u32
|
||||
category: crypto
|
||||
kind: namespace-method
|
||||
tokens: Crypto.random_u32
|
||||
sig: Crypto.random_u32() -> int
|
||||
tip: One CSPRNG-drawn 32-bit integer.
|
||||
order: 8
|
||||
ns: Crypto
|
||||
member: random_u32
|
||||
---
|
||||
|
||||
Draws four bytes from the OS CSPRNG and assembles them into one 32-bit integer. Use it when you need a single unpredictable number rather than a hex string — a random challenge value, a per-run identifier, or a non-deterministic seed to hand to a *fresh* <a href="ns-Random"><code>Random</code></a> stream at startup. Because the bytes come from the secure generator, the value prints as a signed integer and can be negative.
|
||||
|
||||
Like the other secure-random helpers this is **non-deterministic** and must not be called inside the lockstep simulation: doing so desyncs replays and networked peers. Draw it at the edges (startup, on connect) and, if you need reproducible gameplay randomness afterward, seed <a href="random-seed"><code>Random.seed</code></a> with it once.
|
||||
|
||||
```ludic
|
||||
program Challenge {
|
||||
entry {
|
||||
let c = Crypto.random_u32()
|
||||
Random.seed(value: c) # non-deterministic seed, chosen once at startup
|
||||
print(Random.int(100))
|
||||
}
|
||||
}
|
||||
```
|
||||
13
docs/language/uuid/_section.md
Normal file
13
docs/language/uuid/_section.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
id: uuid
|
||||
title: Uuid
|
||||
order: 6
|
||||
---
|
||||
|
||||
Universally-unique identifiers — stable IDs that don't collide, generated locally with no central authority handing out numbers. Reach for a UUID whenever something needs an identity that survives being saved, shared, or sent over a network: players and sessions in multiplayer, user-created content (levels, items, mods) that has to merge cleanly across installs, or a per-install / per-run ID for analytics and bug reports.
|
||||
|
||||
Two versions are provided. <a href="uuid-new"><code>Uuid.new</code></a> makes a **v4** (random) UUID — 122 bits of entropy, effectively never colliding. <a href="uuid-new_v7"><code>Uuid.new_v7</code></a> makes a **v7** (time-ordered) UUID whose leading bits are a millisecond timestamp, so a batch of v7 IDs sorts by creation time — friendly to database indexes and append logs. Both set the RFC 4122 version and variant bits correctly.
|
||||
|
||||
A UUID is represented as its canonical lowercase 36-character text form (<code>8-4-4-4-12</code>), the same shape you store, print, send, and compare — so there is no conversion at each boundary. Validate untrusted input with <a href="uuid-is_valid"><code>Uuid.is_valid</code></a> or normalise it with <a href="uuid-parse"><code>Uuid.parse</code></a>, and compare with <a href="uuid-equals"><code>Uuid.equals</code></a>, which ignores case.
|
||||
|
||||
**Determinism caveat.** v4 and the random tail of v7 come from the OS cryptographically-secure RNG (<a href="ns-Crypto"><code>Crypto</code></a>), which is non-deterministic by design. Minting a UUID inside the lockstep simulation will desync replays and networked peers — generate IDs at the edges (on connect, on save, on spawn-from-input), never per tick in reproducible gameplay code.
|
||||
27
docs/language/uuid/uuid-equals.md
Normal file
27
docs/language/uuid/uuid-equals.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
id: uuid-equals
|
||||
name: Uuid.equals
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.equals
|
||||
sig: Uuid.equals(a, b) -> bool
|
||||
tip: Case-insensitive UUID equality.
|
||||
order: 8
|
||||
ns: Uuid
|
||||
member: equals
|
||||
---
|
||||
|
||||
Compares two UUIDs for equality, ignoring case. UUIDs generated by this library are always lowercase, so a plain <code>==</code> works between them — but a UUID that arrived from another system may be upper- or mixed-case, and <code>equals</code> matches it correctly without you having to normalise first. Values of different length are never equal.
|
||||
|
||||
Parameters:
|
||||
- `a`, `b` — the UUID strings to compare
|
||||
|
||||
```ludic
|
||||
program Eq {
|
||||
entry {
|
||||
let lo = "550e8400-e29b-41d4-a716-446655440000"
|
||||
let up = "550E8400-E29B-41D4-A716-446655440000"
|
||||
if Uuid.equals(lo, up) { print(1) } # case-insensitive
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/uuid/uuid-is_valid.md
Normal file
28
docs/language/uuid/uuid-is_valid.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: uuid-is_valid
|
||||
name: Uuid.is_valid
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.is_valid
|
||||
sig: Uuid.is_valid(s) -> bool
|
||||
tip: Is s a well-formed UUID string?
|
||||
order: 6
|
||||
ns: Uuid
|
||||
member: is_valid
|
||||
---
|
||||
|
||||
Reports whether <code>s</code> is a well-formed UUID: exactly 36 characters, hyphens at positions 8, 13, 18 and 23, and hexadecimal digits (either case) everywhere else. This is the guard to run on any UUID that came from outside your program — a save file, a network message, a mod, a command-line flag — before you trust it as an identity.
|
||||
|
||||
It checks *shape*, not version: both v4 and v7 IDs (and any other conforming UUID) pass. To also fold a malformed value into a safe default in one step, use <a href="uuid-parse"><code>Uuid.parse</code></a> instead.
|
||||
|
||||
Parameters:
|
||||
- `s` — the string to check
|
||||
|
||||
```ludic
|
||||
program Valid {
|
||||
entry {
|
||||
if Uuid.is_valid("550e8400-e29b-41d4-a716-446655440000") { print(1) }
|
||||
if not Uuid.is_valid("not-a-uuid") { print(2) }
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/uuid/uuid-new.md
Normal file
25
docs/language/uuid/uuid-new.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: uuid-new
|
||||
name: Uuid.new
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.new
|
||||
sig: Uuid.new() -> string
|
||||
tip: A new random (v4) UUID as a canonical 36-char string.
|
||||
order: 1
|
||||
ns: Uuid
|
||||
member: new
|
||||
---
|
||||
|
||||
Generates a new **v4** (random) UUID and returns it in canonical lowercase text form, e.g. <code>550e8400-e29b-41d4-a716-446655440000</code>. A v4 UUID carries 122 bits of entropy drawn from the OS secure random generator, so two independently-generated IDs colliding is not something you will ever observe in practice — which is exactly what makes it a good identity for a player, a session, a networked entity, or a piece of user-created content that has no central authority to number it.
|
||||
|
||||
This is the default UUID constructor; <a href="uuid-v4"><code>Uuid.v4</code></a> is an explicit alias. Because the value is random it is **non-deterministic** — do not mint UUIDs inside the lockstep simulation, or replays and peers will diverge.
|
||||
|
||||
```ludic
|
||||
program NewId {
|
||||
entry {
|
||||
let id = Uuid.new()
|
||||
print(len(id)) # 36
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/uuid/uuid-new_v7.md
Normal file
25
docs/language/uuid/uuid-new_v7.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: uuid-new_v7
|
||||
name: Uuid.new_v7
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.new_v7
|
||||
sig: Uuid.new_v7() -> string
|
||||
tip: A new time-ordered (v7) UUID; sorts by creation time.
|
||||
order: 3
|
||||
ns: Uuid
|
||||
member: new_v7
|
||||
---
|
||||
|
||||
Generates a new **v7** (time-ordered) UUID. Its leading 48 bits are a Unix-millisecond timestamp, so a batch of v7 IDs sorts lexicographically by creation time — which keeps database indexes and append-only logs tidy in a way random v4 IDs do not. The remaining bits are secure random, so IDs minted in the same millisecond are still distinct, and the RFC 4122 version (7) and variant bits are set.
|
||||
|
||||
Sub-second resolution is derived from the wall clock in seconds (multiplied to milliseconds), so ordering is guaranteed at one-second granularity with the random tail breaking ties within a second. Like v4 this reads the non-deterministic wall clock and CSPRNG — generate at the edges, never inside reproducible simulation. <a href="uuid-v7"><code>Uuid.v7</code></a> is an alias.
|
||||
|
||||
```ludic
|
||||
program NewV7 {
|
||||
entry {
|
||||
let id = Uuid.new_v7()
|
||||
print(id[14..15]) # 7 — the version nibble
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/uuid/uuid-nil.md
Normal file
23
docs/language/uuid/uuid-nil.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: uuid-nil
|
||||
name: Uuid.nil
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.nil
|
||||
sig: Uuid.nil() -> string
|
||||
tip: The all-zero UUID.
|
||||
order: 9
|
||||
ns: Uuid
|
||||
member: nil
|
||||
---
|
||||
|
||||
Returns the nil UUID — <code>00000000-0000-0000-0000-000000000000</code> — the reserved all-zero value that means "no UUID". Use it as a sentinel for an unset or absent identity, and as the value <a href="uuid-parse"><code>Uuid.parse</code></a> returns when it is handed something that is not a valid UUID. It is a well-formed UUID string, so it passes <a href="uuid-is_valid"><code>Uuid.is_valid</code></a>; test for "no id" by comparing against <code>Uuid.nil()</code> explicitly.
|
||||
|
||||
```ludic
|
||||
program Nil {
|
||||
entry {
|
||||
let none = Uuid.nil()
|
||||
print(none) # 00000000-0000-0000-0000-000000000000
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/uuid/uuid-parse.md
Normal file
29
docs/language/uuid/uuid-parse.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
id: uuid-parse
|
||||
name: Uuid.parse
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.parse
|
||||
sig: Uuid.parse(s) -> string
|
||||
tip: Normalise an untrusted string to a lowercase UUID, or the nil UUID.
|
||||
order: 5
|
||||
ns: Uuid
|
||||
member: parse
|
||||
---
|
||||
|
||||
Normalises an untrusted string: if <code>s</code> is a well-formed UUID it returns the same value in canonical lowercase form; if it is not, it returns the <a href="uuid-nil"><code>nil</code></a> UUID. This never faults on garbage, so it is safe to call on data that arrived from a file, a network peer, or a mod. When you need to *reject* bad input rather than fold it to nil, gate on <a href="uuid-is_valid"><code>Uuid.is_valid</code></a> first.
|
||||
|
||||
Parsing accepts either case and yields the lowercase canonical form, which is what the rest of the library produces — so a parsed ID compares equal (with <code>==</code>) to a freshly generated one of the same value.
|
||||
|
||||
Parameters:
|
||||
- `s` — the string to validate and normalise
|
||||
|
||||
```ludic
|
||||
program Parse {
|
||||
entry {
|
||||
let id = Uuid.parse("550E8400-E29B-41D4-A716-446655440000")
|
||||
print(id[0..8]) # 550e8400 — lowercased
|
||||
if Uuid.parse("nope") == Uuid.nil() { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/uuid/uuid-to_text.md
Normal file
26
docs/language/uuid/uuid-to_text.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: uuid-to_text
|
||||
name: Uuid.to_text
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.to_text
|
||||
sig: Uuid.to_text(id) -> string
|
||||
tip: The canonical text form of a UUID.
|
||||
order: 7
|
||||
ns: Uuid
|
||||
member: to_text
|
||||
---
|
||||
|
||||
Returns the canonical 36-character text form of <code>id</code>. Because this library already represents every UUID as its canonical lowercase text, <code>to_text</code> is the identity function — it exists so intent is explicit at the point where a UUID is turned into a string for display, storage, or a wire payload, and so code stays correct if the internal representation ever changes to a packed 128-bit value.
|
||||
|
||||
Parameters:
|
||||
- `id` — the UUID to render
|
||||
|
||||
```ludic
|
||||
program ToText {
|
||||
entry {
|
||||
let id = Uuid.new()
|
||||
print(len(Uuid.to_text(id))) # 36
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/uuid/uuid-v4.md
Normal file
24
docs/language/uuid/uuid-v4.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
id: uuid-v4
|
||||
name: Uuid.v4
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.v4
|
||||
sig: Uuid.v4() -> string
|
||||
tip: Explicit alias for Uuid.new — a random (v4) UUID.
|
||||
order: 2
|
||||
ns: Uuid
|
||||
member: v4
|
||||
---
|
||||
|
||||
An explicit alias for <a href="uuid-new"><code>Uuid.new</code></a>: generates a **v4** (random) UUID in canonical lowercase text form. Use this spelling when the surrounding code also uses <a href="uuid-v7"><code>Uuid.v7</code></a> and naming both by version reads more clearly than <code>new</code> versus <code>new_v7</code>. The behaviour, entropy, and non-determinism caveat are identical to <code>Uuid.new</code>.
|
||||
|
||||
```ludic
|
||||
program V4 {
|
||||
entry {
|
||||
let a = Uuid.v4()
|
||||
let b = Uuid.v4()
|
||||
if a != b { print(1) } # two draws differ
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/uuid/uuid-v7.md
Normal file
23
docs/language/uuid/uuid-v7.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
id: uuid-v7
|
||||
name: Uuid.v7
|
||||
category: uuid
|
||||
kind: namespace-method
|
||||
tokens: Uuid.v7
|
||||
sig: Uuid.v7() -> string
|
||||
tip: Explicit alias for Uuid.new_v7 — a time-ordered UUID.
|
||||
order: 4
|
||||
ns: Uuid
|
||||
member: v7
|
||||
---
|
||||
|
||||
An explicit alias for <a href="uuid-new_v7"><code>Uuid.new_v7</code></a>: generates a **v7** (time-ordered) UUID whose leading bits are a millisecond timestamp so the IDs sort by creation time. Use this spelling to sit symmetrically beside <a href="uuid-v4"><code>Uuid.v4</code></a>. Behaviour, timestamp resolution, and the determinism caveat are identical to <code>Uuid.new_v7</code>.
|
||||
|
||||
```ludic
|
||||
program V7 {
|
||||
entry {
|
||||
let id = Uuid.v7()
|
||||
if Uuid.is_valid(id) { print(1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
30
examples/library/crypto.ludic
Normal file
30
examples/library/crypto.ludic
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# crypto.ludic — Crypto.* known-answer vectors + the secure-random surface.
|
||||
# SHA-256 / HMAC-SHA256 / base64 are checked against published test vectors, so a
|
||||
# regression in the integer IR shows up as a changed line. The secure-random
|
||||
# helpers are non-deterministic, so we assert their SHAPE (length, distinctness),
|
||||
# never a fixed value. Running it prints: 1 2 3 4 5 6 7 8 9
|
||||
program Crypto {
|
||||
entry {
|
||||
# SHA-256 (FIPS 180-4 examples)
|
||||
if Crypto.sha256("abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" { print(1) }
|
||||
if Crypto.sha256("") == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { print(2) }
|
||||
|
||||
# HMAC-SHA256 (RFC 4231 test case 2: key "Jefe", data "what do ya want for nothing?")
|
||||
if Crypto.hmac_sha256("Jefe", "what do ya want for nothing?") == "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" { print(3) }
|
||||
|
||||
# constant-time verify: the right MAC verifies, a tampered one does not
|
||||
let mac = Crypto.hmac_sha256("k", "payload")
|
||||
if Crypto.verify_hmac("k", "payload", mac) { print(4) }
|
||||
if not Crypto.verify_hmac("k", "payload", "00") { print(5) }
|
||||
|
||||
# base64 (RFC 4648 vectors), covering every padding remainder
|
||||
if Crypto.base64("") == "" { print(6) }
|
||||
if Crypto.base64("f") == "Zg==" { print(7) }
|
||||
if Crypto.base64("foobar") == "Zm9vYmFy" { print(8) }
|
||||
|
||||
# secure random: two 16-byte draws are 32 hex chars each and (near-certainly) differ
|
||||
let a = Crypto.random_bytes(16)
|
||||
let b = Crypto.random_bytes(16)
|
||||
if len(a) == 32 and a != b { print(9) }
|
||||
}
|
||||
}
|
||||
32
examples/library/uuid.ludic
Normal file
32
examples/library/uuid.ludic
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# uuid.ludic — Uuid.* format round-trip, version/variant bits, validation and
|
||||
# equality. v4/v7 values are random, so we assert their STRUCTURE (length, the
|
||||
# version nibble, the variant, that two draws differ), never a fixed value.
|
||||
# Running it prints: 1 2 3 4 5 6 7 8 9 10
|
||||
program Uuid {
|
||||
entry {
|
||||
let a = Uuid.new()
|
||||
let b = Uuid.new()
|
||||
|
||||
# 36 chars, canonical hyphen positions, and two draws differ
|
||||
if len(a) == 36 { print(1) }
|
||||
if a[8..9] == "-" and a[13..14] == "-" and a[18..19] == "-" and a[23..24] == "-" { print(2) }
|
||||
if a != b { print(3) }
|
||||
|
||||
# v4: version nibble is '4' (index 14), variant is one of 8/9/a/b (index 19)
|
||||
if a[14..15] == "4" { print(4) }
|
||||
let var4 = a[19..20]
|
||||
if var4 == "8" or var4 == "9" or var4 == "a" or var4 == "b" { print(5) }
|
||||
|
||||
# v7: version nibble is '7'; still a well-formed UUID
|
||||
let c = Uuid.new_v7()
|
||||
if c[14..15] == "7" and Uuid.is_valid(c) { print(6) }
|
||||
|
||||
# validation of untrusted input
|
||||
if Uuid.is_valid("550e8400-e29b-41d4-a716-446655440000") { print(7) }
|
||||
if not Uuid.is_valid("not-a-uuid") { print(8) }
|
||||
|
||||
# parse normalises case; equals is case-insensitive; bad input -> nil
|
||||
if Uuid.equals(Uuid.parse("550E8400-E29B-41D4-A716-446655440000"), "550e8400-e29b-41d4-a716-446655440000") { print(9) }
|
||||
if Uuid.parse("garbage") == Uuid.nil() { print(10) }
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ var g_uses_textrt: bool = false # Text.upper/lower/trim/repeat/pad was emitted
|
|||
var g_uses_textrt2: bool = false # Text.split/join/replace was emitted -> emit the string/slice builders
|
||||
var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers
|
||||
var g_uses_cryptort: bool = false # Crypto.* was emitted -> emit the SHA-256 / HMAC runtime
|
||||
var g_uses_uuidrt: bool = false # Uuid.* was emitted -> emit the UUID runtime (needs the crypto CSPRNG)
|
||||
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
|
||||
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@
|
|||
# in constant time -> bool (the tamper check)
|
||||
# Crypto.hex(s) lowercase hex of the bytes of `s`
|
||||
# Crypto.ct_equal(a, b) constant-time string equality (for secrets/MACs)
|
||||
# Crypto.random_bytes(n) n bytes from the OS CSPRNG -> 2n-char hex string
|
||||
# Crypto.random_hex(n) alias for random_bytes (explicit about the return)
|
||||
# Crypto.random_u32() one CSPRNG-drawn 32-bit int (tokens, non-sim seeds)
|
||||
# Crypto.base64(s) standard base64 (RFC 4648) of the bytes of `s`
|
||||
#
|
||||
# The secure-random helpers read the operating system CSPRNG (/dev/urandom) and
|
||||
# are deliberately NON-deterministic — never seed the lockstep simulation RNG
|
||||
# from them (that is `Random.*`). They are for tokens, nonces, and UUIDs, whose
|
||||
# whole point is unpredictability. On a target without /dev/urandom (e.g. wasm)
|
||||
# the read yields zeroes; a real CSPRNG binding is left to the platform layer.
|
||||
#
|
||||
# This is a well-specified standard algorithm (FIPS 180-4 / RFC 2104), implemented
|
||||
# from scratch in plain integer IR: no libc crypto, no allocation-order or
|
||||
|
|
@ -26,6 +36,8 @@
|
|||
function is_crypto_ns(meth: pointer) -> bool {
|
||||
if (meth == "sha256") or (meth == "hmac_sha256") or (meth == "verify_hmac") { return true }
|
||||
if (meth == "hex") or (meth == "ct_equal") { return true }
|
||||
if (meth == "random_bytes") or (meth == "random_hex") or (meth == "random_u32") { return true }
|
||||
if (meth == "base64") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +59,21 @@ function emit_crypto_ns(meth: pointer, e: Node) -> Val {
|
|||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call i32 @fn_ct_streq(ptr {a.code}, ptr {b.code})`), "bool")
|
||||
}
|
||||
# random_bytes(n) / random_hex(n): n bytes from the OS CSPRNG, returned as a
|
||||
# 2n-char lowercase hex string. A digest of raw bytes can contain NUL and a
|
||||
# `str` is NUL-terminated, so the secure-random surface is hex like the digests.
|
||||
if (meth == "random_bytes") or (meth == "random_hex") {
|
||||
let n = emit_expr(e.kids[0])
|
||||
let n64 = emit_bind(`sext i32 {n.code} to i64`)
|
||||
return val(emit_bind(`call ptr @fn_random_hex(i64 {n64})`), "string")
|
||||
}
|
||||
if (meth == "random_u32") { # one CSPRNG-drawn 32-bit int
|
||||
return val(emit_bind(`call i32 @fn_random_u32()`), "int")
|
||||
}
|
||||
if (meth == "base64") { # standard base64 (RFC 4648) of a string's bytes
|
||||
let s = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @fn_base64(ptr {s.code})`), "string")
|
||||
}
|
||||
# verify_hmac(key, msg, mac): recompute HMAC-SHA256(key, msg) and compare it to
|
||||
# the supplied hex `mac` in constant time. This is the safe way to check a MAC —
|
||||
# `==` would leak, byte by byte, how much of a forged MAC was correct.
|
||||
|
|
@ -253,4 +280,72 @@ function emit_crypto_prelude() -> void {
|
|||
emith("c:\n %i = load i64, ptr %ip\n %lt = icmp ult i64 %i, %la\n br i1 %lt, label %bdy, label %d\n")
|
||||
emith("bdy:\n %pa = getelementptr i8, ptr %a, i64 %i\n %va = load i8, ptr %pa\n %pb = getelementptr i8, ptr %b, i64 %i\n %vb = load i8, ptr %pb\n %x = xor i8 %va, %vb\n %xz = zext i8 %x to i32\n %ac = load i32, ptr %accp\n %ao = or i32 %ac, %xz\n store i32 %ao, ptr %accp\n %in = add i64 %i, 1\n store i64 %in, ptr %ip\n br label %c\n")
|
||||
emith("d:\n %finv = load i32, ptr %accp\n %z = icmp eq i32 %finv, 0\n %r = zext i1 %z to i32\n ret i32 %r\n}\n")
|
||||
|
||||
emit_secure_rand_prelude()
|
||||
}
|
||||
|
||||
# emit_secure_rand_prelude — the OS-CSPRNG surface shared by Crypto.random_* and
|
||||
# the Uuid.* library: read raw bytes from /dev/urandom, and a base64 encoder.
|
||||
# Emitted as part of the crypto prelude (both Crypto.* and Uuid.* set
|
||||
# g_uses_cryptort), so hex_encode above is always in scope here.
|
||||
function emit_secure_rand_prelude() -> void {
|
||||
emith("@.ludic_urandom = private unnamed_addr constant [13 x i8] c\"/dev/urandom\\00\"\n")
|
||||
emith("@.ludic_rbmode = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
|
||||
emith("@.ludic_b64tab = private unnamed_addr constant [64 x i8] c\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n")
|
||||
|
||||
# fill %n bytes at %out from the OS CSPRNG. If /dev/urandom cannot be opened the
|
||||
# buffer is zeroed (documented degraded mode — e.g. wasm), never left uninit.
|
||||
emith("define void @fn_secure_bytes(ptr %out, i64 %n) {\n")
|
||||
emith("entry:\n call ptr @memset(ptr %out, i32 0, i64 %n)\n")
|
||||
emith(" %fp = call ptr @fopen(ptr @.ludic_urandom, ptr @.ludic_rbmode)\n")
|
||||
emith(" %isnull = icmp eq ptr %fp, null\n br i1 %isnull, label %fail, label %ok\n")
|
||||
emith("ok:\n %rd = call i64 @fread(ptr %out, i64 1, i64 %n, ptr %fp)\n %cl = call i32 @fclose(ptr %fp)\n ret void\n")
|
||||
emith("fail:\n ret void\n}\n")
|
||||
|
||||
# %n secure bytes -> a fresh 2n-char lowercase hex string
|
||||
emith("define ptr @fn_random_hex(i64 %n) {\n")
|
||||
emith("entry:\n %buf = call ptr @malloc(i64 %n)\n call void @fn_secure_bytes(ptr %buf, i64 %n)\n")
|
||||
emith(" %hex = call ptr @fn_hex_encode(ptr %buf, i64 %n)\n call void @free(ptr %buf)\n ret ptr %hex\n}\n")
|
||||
|
||||
# one CSPRNG-drawn i32 (little-endian assembly of four secure bytes)
|
||||
emith("define i32 @fn_random_u32() {\n")
|
||||
emith("entry:\n %b = alloca [4 x i8]\n %bp = getelementptr [4 x i8], ptr %b, i64 0, i64 0\n call void @fn_secure_bytes(ptr %bp, i64 4)\n")
|
||||
emith(" %p0 = getelementptr i8, ptr %bp, i64 0\n %c0 = load i8, ptr %p0\n %z0 = zext i8 %c0 to i32\n")
|
||||
emith(" %p1 = getelementptr i8, ptr %bp, i64 1\n %c1 = load i8, ptr %p1\n %z1 = zext i8 %c1 to i32\n %s1 = shl i32 %z1, 8\n")
|
||||
emith(" %p2 = getelementptr i8, ptr %bp, i64 2\n %c2 = load i8, ptr %p2\n %z2 = zext i8 %c2 to i32\n %s2 = shl i32 %z2, 16\n")
|
||||
emith(" %p3 = getelementptr i8, ptr %bp, i64 3\n %c3 = load i8, ptr %p3\n %z3 = zext i8 %c3 to i32\n %s3 = shl i32 %z3, 24\n")
|
||||
emith(" %o1 = or i32 %z0, %s1\n %o2 = or i32 %o1, %s2\n %o3 = or i32 %o2, %s3\n ret i32 %o3\n}\n")
|
||||
|
||||
# standard base64 (RFC 4648, '+' '/' alphabet, '=' padding). The input is copied
|
||||
# into a zero-padded buffer rounded up to a multiple of 3, so the 3-byte group
|
||||
# loop never reads past the string; trailing '=' are written per the remainder.
|
||||
emith("define ptr @fn_base64(ptr %s) {\n")
|
||||
emith("entry:\n %n = call i64 @strlen(ptr %s)\n")
|
||||
emith(" %n2 = add i64 %n, 2\n %grp = udiv i64 %n2, 3\n %bufn = mul i64 %grp, 3\n")
|
||||
emith(" %olen = mul i64 %grp, 4\n %olen1 = add i64 %olen, 1\n %out = call ptr @malloc(i64 %olen1)\n")
|
||||
emith(" %inbuf = call ptr @malloc(i64 %bufn)\n call ptr @memset(ptr %inbuf, i32 0, i64 %bufn)\n call ptr @memcpy(ptr %inbuf, ptr %s, i64 %n)\n")
|
||||
emith(" %gp = alloca i64\n store i64 0, ptr %gp\n br label %cond\n")
|
||||
emith("cond:\n %gi = load i64, ptr %gp\n %lt = icmp ult i64 %gi, %grp\n br i1 %lt, label %body, label %pad\n")
|
||||
emith("body:\n %i3 = mul i64 %gi, 3\n")
|
||||
emith(" %ip0 = getelementptr i8, ptr %inbuf, i64 %i3\n %bv0 = load i8, ptr %ip0\n %b0 = zext i8 %bv0 to i32\n")
|
||||
emith(" %i3a = add i64 %i3, 1\n %ip1 = getelementptr i8, ptr %inbuf, i64 %i3a\n %bv1 = load i8, ptr %ip1\n %b1 = zext i8 %bv1 to i32\n")
|
||||
emith(" %i3b = add i64 %i3, 2\n %ip2 = getelementptr i8, ptr %inbuf, i64 %i3b\n %bv2 = load i8, ptr %ip2\n %b2 = zext i8 %bv2 to i32\n")
|
||||
emith(" %e0 = lshr i32 %b0, 2\n")
|
||||
emith(" %b0l = and i32 %b0, 3\n %b0s = shl i32 %b0l, 4\n %b1h = lshr i32 %b1, 4\n %e1 = or i32 %b0s, %b1h\n")
|
||||
emith(" %b1l = and i32 %b1, 15\n %b1s = shl i32 %b1l, 2\n %b2h = lshr i32 %b2, 6\n %e2 = or i32 %b1s, %b2h\n")
|
||||
emith(" %e3 = and i32 %b2, 63\n")
|
||||
emith(" %o4 = mul i64 %gi, 4\n")
|
||||
emith(" %e0z = zext i32 %e0 to i64\n %e1z = zext i32 %e1 to i64\n %e2z = zext i32 %e2 to i64\n %e3z = zext i32 %e3 to i64\n")
|
||||
emith(" %g0 = getelementptr [64 x i8], ptr @.ludic_b64tab, i64 0, i64 %e0z\n %ch0 = load i8, ptr %g0\n %op0 = getelementptr i8, ptr %out, i64 %o4\n store i8 %ch0, ptr %op0\n")
|
||||
emith(" %o4a = add i64 %o4, 1\n %g1 = getelementptr [64 x i8], ptr @.ludic_b64tab, i64 0, i64 %e1z\n %ch1 = load i8, ptr %g1\n %op1 = getelementptr i8, ptr %out, i64 %o4a\n store i8 %ch1, ptr %op1\n")
|
||||
emith(" %o4b = add i64 %o4, 2\n %g2 = getelementptr [64 x i8], ptr @.ludic_b64tab, i64 0, i64 %e2z\n %ch2 = load i8, ptr %g2\n %op2 = getelementptr i8, ptr %out, i64 %o4b\n store i8 %ch2, ptr %op2\n")
|
||||
emith(" %o4c = add i64 %o4, 3\n %g3 = getelementptr [64 x i8], ptr @.ludic_b64tab, i64 0, i64 %e3z\n %ch3 = load i8, ptr %g3\n %op3 = getelementptr i8, ptr %out, i64 %o4c\n store i8 %ch3, ptr %op3\n")
|
||||
emith(" %gin = add i64 %gi, 1\n store i64 %gin, ptr %gp\n br label %cond\n")
|
||||
emith("pad:\n %rem = urem i64 %n, 3\n %r1 = icmp eq i64 %rem, 1\n %r2 = icmp eq i64 %rem, 2\n")
|
||||
emith(" %eq = getelementptr i8, ptr %out, i64 %olen\n store i8 0, ptr %eq\n")
|
||||
emith(" br i1 %r1, label %pad1, label %maybe2\n")
|
||||
emith("pad1:\n %pm1 = sub i64 %olen, 1\n %pp1 = getelementptr i8, ptr %out, i64 %pm1\n store i8 61, ptr %pp1\n %pm2 = sub i64 %olen, 2\n %pp2 = getelementptr i8, ptr %out, i64 %pm2\n store i8 61, ptr %pp2\n br label %done\n")
|
||||
emith("maybe2:\n br i1 %r2, label %pad2, label %done\n")
|
||||
emith("pad2:\n %qm1 = sub i64 %olen, 1\n %qp1 = getelementptr i8, ptr %out, i64 %qm1\n store i8 61, ptr %qp1\n br label %done\n")
|
||||
emith("done:\n call void @free(ptr %inbuf)\n ret ptr %out\n}\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ function emit_program() -> void {
|
|||
if g_uses_textrt { emit_text_prelude() } # @fn_str_upper/lower/trim/repeat/pad builders
|
||||
if g_uses_textrt2 { emit_text2_prelude() } # @fn_str_replace/join/split builders
|
||||
if g_uses_hashrt { emit_hash_prelude() } # @fn_hash_fnv1a / @fn_hash_crc32 byte hashers
|
||||
if g_uses_cryptort { emit_crypto_prelude() } # @fn_sha256_hex / @fn_hmac_sha256_hex + constant-time compare
|
||||
if g_uses_cryptort { emit_crypto_prelude() } # @fn_sha256_hex / @fn_hmac_sha256_hex + constant-time compare + CSPRNG
|
||||
if g_uses_uuidrt { emit_uuid_prelude() } # @fn_uuid_v4 / @fn_uuid_v7 / parse / equals (over the crypto CSPRNG)
|
||||
if g_uses_datert { emit_datetime_prelude() } # @fn_days_from_civil / @fn_civil_from_days conversions
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
|
||||
perr(`unknown builtin Crypto.{meth}`)
|
||||
}
|
||||
if (ns == "Uuid") {
|
||||
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
|
||||
perr(`unknown builtin Uuid.{meth}`)
|
||||
}
|
||||
if (ns == "Vector") {
|
||||
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
|
||||
perr(`unknown builtin Vector.{meth}`)
|
||||
|
|
|
|||
164
selfhost/emit_uuid.ludic
Normal file
164
selfhost/emit_uuid.ludic
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# emit_uuid.ludic — the Uuid.* namespace: universally-unique identifiers for
|
||||
# stable IDs that don't collide (players, sessions, networked entities, saved
|
||||
# and shared user content, per-install analytics IDs).
|
||||
#
|
||||
# Uuid.new() a v4 (random) UUID -> canonical 36-char string
|
||||
# Uuid.v4() explicit alias for new()
|
||||
# Uuid.new_v7() a v7 (time-ordered) UUID: the first 48 bits are a Unix-ms
|
||||
# Uuid.v7() timestamp, so v7 IDs sort by creation time (DB/index-friendly)
|
||||
# Uuid.parse(s) normalise an untrusted string -> lowercase UUID, or the nil
|
||||
# UUID if it is not a well-formed UUID (pair with Uuid.is_valid)
|
||||
# Uuid.is_valid(s) is `s` a well-formed UUID? -> bool
|
||||
# Uuid.to_text(id) the canonical text form -> string (identity here)
|
||||
# Uuid.equals(a, b) case-insensitive equality -> bool
|
||||
# Uuid.nil() the all-zero UUID -> "00000000-0000-0000-0000-000000000000"
|
||||
#
|
||||
# A UUID is represented as its canonical lowercase 36-char text form. This is the
|
||||
# form you store, print, send over the wire and compare, so keeping IDs in that
|
||||
# shape avoids a conversion at every boundary; `equals` is case-insensitive so an
|
||||
# upper-case UUID from another system still matches.
|
||||
#
|
||||
# DETERMINISM CAVEAT: v4 and the random tail of v7 come from the OS CSPRNG
|
||||
# (Crypto.random_*), which is deliberately non-deterministic. Minting a UUID
|
||||
# inside the lockstep simulation will desync replays / networked peers — generate
|
||||
# IDs at the edges (on connect, on save, on spawn-from-input), never per tick in
|
||||
# gameplay code that must reproduce.
|
||||
|
||||
function is_uuid_ns(meth: pointer) -> bool {
|
||||
if (meth == "new") or (meth == "v4") or (meth == "new_v7") or (meth == "v7") { return true }
|
||||
if (meth == "parse") or (meth == "is_valid") or (meth == "to_text") { return true }
|
||||
if (meth == "equals") or (meth == "nil") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
function emit_uuid_ns(meth: pointer, e: Node) -> Val {
|
||||
# Uuid.* reuses the crypto prelude's CSPRNG (fn_secure_bytes) and hex encoder
|
||||
# (fn_hex_encode), so pull that runtime in as well as the uuid formatters.
|
||||
g_uses_cryptort = true
|
||||
g_uses_uuidrt = true
|
||||
if (meth == "new") or (meth == "v4") { # v4: 122 random bits
|
||||
return val(emit_bind("call ptr @fn_uuid_v4()"), "string")
|
||||
}
|
||||
if (meth == "new_v7") or (meth == "v7") { # v7: ms timestamp + random
|
||||
return val(emit_bind("call ptr @fn_uuid_v7()"), "string")
|
||||
}
|
||||
if (meth == "nil") { # the all-zero UUID
|
||||
return val(emit_bind("call ptr @fn_uuid_nil()"), "string")
|
||||
}
|
||||
if (meth == "is_valid") { # well-formed UUID? -> bool
|
||||
let s = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call i32 @fn_uuid_valid(ptr {s.code})`), "bool")
|
||||
}
|
||||
if (meth == "to_text") { # already canonical text: identity
|
||||
let s = emit_expr(e.kids[0])
|
||||
return val(s.code, "string")
|
||||
}
|
||||
if (meth == "equals") { # case-insensitive equality -> bool
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call i32 @fn_uuid_eq(ptr {a.code}, ptr {b.code})`), "bool")
|
||||
}
|
||||
# parse(s): normalise an untrusted string to a lowercase UUID, or the nil UUID
|
||||
# when it is not well-formed. Callers that must reject bad input should gate on
|
||||
# Uuid.is_valid(s) first; this never faults on garbage.
|
||||
let s = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @fn_uuid_parse(ptr {s.code})`), "string")
|
||||
}
|
||||
|
||||
# emit_uuid_prelude — the UUID runtime, emitted once per program that uses Uuid.*
|
||||
# (g_uses_uuidrt). All pure integer IR over the crypto prelude's CSPRNG + hex
|
||||
# encoder; format is the canonical 8-4-4-4-12 lowercase text with RFC 4122
|
||||
# version and variant bits set.
|
||||
function emit_uuid_prelude() -> void {
|
||||
# 16 raw bytes -> a fresh canonical 36-char string. hex-encode all 16 bytes,
|
||||
# then splice the four hyphens between the 8/4/4/4/12 groups.
|
||||
emith("define ptr @fn_uuid_format(ptr %b16) {\n")
|
||||
emith("entry:\n %hex = call ptr @fn_hex_encode(ptr %b16, i64 16)\n %out = call ptr @malloc(i64 37)\n")
|
||||
emith(" call ptr @memcpy(ptr %out, ptr %hex, i64 8)\n")
|
||||
emith(" %o8 = getelementptr i8, ptr %out, i64 8\n store i8 45, ptr %o8\n")
|
||||
emith(" %h8 = getelementptr i8, ptr %hex, i64 8\n %o9 = getelementptr i8, ptr %out, i64 9\n call ptr @memcpy(ptr %o9, ptr %h8, i64 4)\n")
|
||||
emith(" %o13 = getelementptr i8, ptr %out, i64 13\n store i8 45, ptr %o13\n")
|
||||
emith(" %h12 = getelementptr i8, ptr %hex, i64 12\n %o14 = getelementptr i8, ptr %out, i64 14\n call ptr @memcpy(ptr %o14, ptr %h12, i64 4)\n")
|
||||
emith(" %o18 = getelementptr i8, ptr %out, i64 18\n store i8 45, ptr %o18\n")
|
||||
emith(" %h16 = getelementptr i8, ptr %hex, i64 16\n %o19 = getelementptr i8, ptr %out, i64 19\n call ptr @memcpy(ptr %o19, ptr %h16, i64 4)\n")
|
||||
emith(" %o23 = getelementptr i8, ptr %out, i64 23\n store i8 45, ptr %o23\n")
|
||||
emith(" %h20 = getelementptr i8, ptr %hex, i64 20\n %o24 = getelementptr i8, ptr %out, i64 24\n call ptr @memcpy(ptr %o24, ptr %h20, i64 12)\n")
|
||||
emith(" %o36 = getelementptr i8, ptr %out, i64 36\n store i8 0, ptr %o36\n")
|
||||
emith(" call void @free(ptr %hex)\n ret ptr %out\n}\n")
|
||||
|
||||
# v4: 16 CSPRNG bytes, then set version (0x4x in byte 6) and variant (0b10xx in
|
||||
# byte 8). 0x80 does not fit an i8 immediate, so it is written as -128.
|
||||
emith("define ptr @fn_uuid_v4() {\n")
|
||||
emith("entry:\n %b = alloca [16 x i8]\n %bp = getelementptr [16 x i8], ptr %b, i64 0, i64 0\n call void @fn_secure_bytes(ptr %bp, i64 16)\n")
|
||||
emith(" %p6 = getelementptr i8, ptr %bp, i64 6\n %v6 = load i8, ptr %p6\n %v6a = and i8 %v6, 15\n %v6b = or i8 %v6a, 64\n store i8 %v6b, ptr %p6\n")
|
||||
emith(" %p8 = getelementptr i8, ptr %bp, i64 8\n %v8 = load i8, ptr %p8\n %v8a = and i8 %v8, 63\n %v8b = or i8 %v8a, -128\n store i8 %v8b, ptr %p8\n")
|
||||
emith(" %s = call ptr @fn_uuid_format(ptr %bp)\n ret ptr %s\n}\n")
|
||||
|
||||
# v7: random fill, then overwrite the first 6 bytes with a 48-bit big-endian
|
||||
# Unix-millisecond timestamp; set version 7 (0x7x) and the variant. Sub-second
|
||||
# resolution is derived from time() seconds * 1000 — monotonic per second, with
|
||||
# the random tail keeping same-millisecond IDs distinct.
|
||||
emith("define ptr @fn_uuid_v7() {\n")
|
||||
emith("entry:\n %b = alloca [16 x i8]\n %bp = getelementptr [16 x i8], ptr %b, i64 0, i64 0\n call void @fn_secure_bytes(ptr %bp, i64 16)\n")
|
||||
emith(" %t = call i64 @time(ptr null)\n %ms = mul i64 %t, 1000\n")
|
||||
emith(" %s40 = lshr i64 %ms, 40\n %t0 = trunc i64 %s40 to i8\n %q0 = getelementptr i8, ptr %bp, i64 0\n store i8 %t0, ptr %q0\n")
|
||||
emith(" %s32 = lshr i64 %ms, 32\n %t1 = trunc i64 %s32 to i8\n %q1 = getelementptr i8, ptr %bp, i64 1\n store i8 %t1, ptr %q1\n")
|
||||
emith(" %s24 = lshr i64 %ms, 24\n %t2 = trunc i64 %s24 to i8\n %q2 = getelementptr i8, ptr %bp, i64 2\n store i8 %t2, ptr %q2\n")
|
||||
emith(" %s16 = lshr i64 %ms, 16\n %t3 = trunc i64 %s16 to i8\n %q3 = getelementptr i8, ptr %bp, i64 3\n store i8 %t3, ptr %q3\n")
|
||||
emith(" %s8 = lshr i64 %ms, 8\n %t4 = trunc i64 %s8 to i8\n %q4 = getelementptr i8, ptr %bp, i64 4\n store i8 %t4, ptr %q4\n")
|
||||
emith(" %t5 = trunc i64 %ms to i8\n %q5 = getelementptr i8, ptr %bp, i64 5\n store i8 %t5, ptr %q5\n")
|
||||
emith(" %p6 = getelementptr i8, ptr %bp, i64 6\n %v6 = load i8, ptr %p6\n %v6a = and i8 %v6, 15\n %v6b = or i8 %v6a, 112\n store i8 %v6b, ptr %p6\n")
|
||||
emith(" %p8 = getelementptr i8, ptr %bp, i64 8\n %v8 = load i8, ptr %p8\n %v8a = and i8 %v8, 63\n %v8b = or i8 %v8a, -128\n store i8 %v8b, ptr %p8\n")
|
||||
emith(" %s = call ptr @fn_uuid_format(ptr %bp)\n ret ptr %s\n}\n")
|
||||
|
||||
# the nil UUID: 36 '0' with hyphens spliced in
|
||||
emith("define ptr @fn_uuid_nil() {\n")
|
||||
emith("entry:\n %out = call ptr @malloc(i64 37)\n call ptr @memset(ptr %out, i32 48, i64 36)\n")
|
||||
emith(" %o8 = getelementptr i8, ptr %out, i64 8\n store i8 45, ptr %o8\n")
|
||||
emith(" %o13 = getelementptr i8, ptr %out, i64 13\n store i8 45, ptr %o13\n")
|
||||
emith(" %o18 = getelementptr i8, ptr %out, i64 18\n store i8 45, ptr %o18\n")
|
||||
emith(" %o23 = getelementptr i8, ptr %out, i64 23\n store i8 45, ptr %o23\n")
|
||||
emith(" %o36 = getelementptr i8, ptr %out, i64 36\n store i8 0, ptr %o36\n ret ptr %out\n}\n")
|
||||
|
||||
# is %s a well-formed UUID? length 36, hyphens at 8/13/18/23, hex elsewhere.
|
||||
emith("define i32 @fn_uuid_valid(ptr %s) {\n")
|
||||
emith("entry:\n %n = call i64 @strlen(ptr %s)\n %ne = icmp eq i64 %n, 36\n br i1 %ne, label %go, label %bad\n")
|
||||
emith("go:\n %ip = alloca i64\n store i64 0, ptr %ip\n br label %cond\n")
|
||||
emith("cond:\n %i = load i64, ptr %ip\n %lt = icmp ult i64 %i, 36\n br i1 %lt, label %body, label %good\n")
|
||||
emith("body:\n %p = getelementptr i8, ptr %s, i64 %i\n %c = load i8, ptr %p\n")
|
||||
emith(" %h8 = icmp eq i64 %i, 8\n %h13 = icmp eq i64 %i, 13\n %h18 = icmp eq i64 %i, 18\n %h23 = icmp eq i64 %i, 23\n")
|
||||
emith(" %hx = or i1 %h8, %h13\n %hy = or i1 %hx, %h18\n %hyph = or i1 %hy, %h23\n br i1 %hyph, label %ckhyph, label %ckhex\n")
|
||||
emith("ckhyph:\n %ish = icmp eq i8 %c, 45\n br i1 %ish, label %next, label %bad\n")
|
||||
emith("ckhex:\n")
|
||||
emith(" %ge0 = icmp uge i8 %c, 48\n %le9 = icmp ule i8 %c, 57\n %isdig = and i1 %ge0, %le9\n")
|
||||
emith(" %gea = icmp uge i8 %c, 97\n %lef = icmp ule i8 %c, 102\n %islo = and i1 %gea, %lef\n")
|
||||
emith(" %geA = icmp uge i8 %c, 65\n %leF = icmp ule i8 %c, 70\n %ishi = and i1 %geA, %leF\n")
|
||||
emith(" %hx1 = or i1 %isdig, %islo\n %ishex = or i1 %hx1, %ishi\n br i1 %ishex, label %next, label %bad\n")
|
||||
emith("next:\n %i1 = add i64 %i, 1\n store i64 %i1, ptr %ip\n br label %cond\n")
|
||||
emith("good:\n ret i32 1\n")
|
||||
emith("bad:\n ret i32 0\n}\n")
|
||||
|
||||
# case-insensitive equality of two null-terminated strings -> i32 bool
|
||||
emith("define i32 @fn_uuid_eq(ptr %a, ptr %b) {\n")
|
||||
emith("entry:\n %la = call i64 @strlen(ptr %a)\n %lb = call i64 @strlen(ptr %b)\n %eq = icmp eq i64 %la, %lb\n br i1 %eq, label %go, label %ne\n")
|
||||
emith("go:\n %ip = alloca i64\n store i64 0, ptr %ip\n br label %cond\n")
|
||||
emith("cond:\n %i = load i64, ptr %ip\n %lt = icmp ult i64 %i, %la\n br i1 %lt, label %body, label %eqret\n")
|
||||
emith("body:\n %pa = getelementptr i8, ptr %a, i64 %i\n %ca = load i8, ptr %pa\n %pb = getelementptr i8, ptr %b, i64 %i\n %cb = load i8, ptr %pb\n")
|
||||
emith(" %caA = icmp uge i8 %ca, 65\n %caZ = icmp ule i8 %ca, 90\n %caup = and i1 %caA, %caZ\n %ca32 = add i8 %ca, 32\n %cal = select i1 %caup, i8 %ca32, i8 %ca\n")
|
||||
emith(" %cbA = icmp uge i8 %cb, 65\n %cbZ = icmp ule i8 %cb, 90\n %cbup = and i1 %cbA, %cbZ\n %cb32 = add i8 %cb, 32\n %cbl = select i1 %cbup, i8 %cb32, i8 %cb\n")
|
||||
emith(" %same = icmp eq i8 %cal, %cbl\n br i1 %same, label %next, label %ne\n")
|
||||
emith("next:\n %i1 = add i64 %i, 1\n store i64 %i1, ptr %ip\n br label %cond\n")
|
||||
emith("eqret:\n ret i32 1\n")
|
||||
emith("ne:\n ret i32 0\n}\n")
|
||||
|
||||
# parse: lowercase-normalise a valid UUID, else return the nil UUID.
|
||||
emith("define ptr @fn_uuid_parse(ptr %s) {\n")
|
||||
emith("entry:\n %ok = call i32 @fn_uuid_valid(ptr %s)\n %isok = icmp ne i32 %ok, 0\n br i1 %isok, label %dup, label %nilb\n")
|
||||
emith("dup:\n %out = call ptr @malloc(i64 37)\n %ip = alloca i64\n store i64 0, ptr %ip\n br label %cond\n")
|
||||
emith("cond:\n %i = load i64, ptr %ip\n %lt = icmp ult i64 %i, 36\n br i1 %lt, label %body, label %fin\n")
|
||||
emith("body:\n %p = getelementptr i8, ptr %s, i64 %i\n %c = load i8, ptr %p\n")
|
||||
emith(" %cA = icmp uge i8 %c, 65\n %cZ = icmp ule i8 %c, 90\n %cup = and i1 %cA, %cZ\n %c32 = add i8 %c, 32\n %cl = select i1 %cup, i8 %c32, i8 %c\n")
|
||||
emith(" %op = getelementptr i8, ptr %out, i64 %i\n store i8 %cl, ptr %op\n")
|
||||
emith(" %i1 = add i64 %i, 1\n store i64 %i1, ptr %ip\n br label %cond\n")
|
||||
emith("fin:\n %o36 = getelementptr i8, ptr %out, i64 36\n store i8 0, ptr %o36\n ret ptr %out\n")
|
||||
emith("nilb:\n %nn = call ptr @fn_uuid_nil()\n ret ptr %nn\n}\n")
|
||||
}
|
||||
22200
selfhost/ludicc.seed.ll
22200
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -404,6 +404,21 @@
|
|||
"crypto-hmac_sha256",
|
||||
"crypto-verify_hmac",
|
||||
"crypto-hex",
|
||||
"crypto-ct_equal"
|
||||
"crypto-ct_equal",
|
||||
"crypto-random_bytes",
|
||||
"crypto-random_hex",
|
||||
"crypto-random_u32",
|
||||
"crypto-base64"
|
||||
],
|
||||
"uuid": [
|
||||
"uuid-new",
|
||||
"uuid-v4",
|
||||
"uuid-new_v7",
|
||||
"uuid-v7",
|
||||
"uuid-parse",
|
||||
"uuid-is_valid",
|
||||
"uuid-to_text",
|
||||
"uuid-equals",
|
||||
"uuid-nil"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/emit_text.ludic")
|
||||
push(f, "selfhost/emit_hash.ludic")
|
||||
push(f, "selfhost/emit_crypto.ludic")
|
||||
push(f, "selfhost/emit_uuid.ludic")
|
||||
push(f, "selfhost/emit_list.ludic")
|
||||
push(f, "selfhost/emit_ease.ludic")
|
||||
push(f, "selfhost/emit_collide.ludic")
|
||||
|
|
|
|||
|
|
@ -97,6 +97,11 @@ function cmd_test() -> int {
|
|||
feat_case("lang/detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
|
||||
feat_case("lang/reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
|
||||
|
||||
# standard-library namespaces, each a self-contained `entry` program asserted
|
||||
# against known-answer vectors (crypto) or structural invariants (uuid/noise).
|
||||
feat_case("library/crypto", "", "1 2 3 4 5 6 7 8 9", "crypto.ludic (Crypto SHA-256/HMAC/base64 KAT + CSPRNG shape)")
|
||||
feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)")
|
||||
|
||||
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
|
||||
net_case("lang/offline_rewards", "13 650 2026-08-30 0")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue