identifier: std/identifier with Uuid and Ulid #122
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Identifier,Uuid,Ulid,IdentifierError, theurn:uuid:bridge.Source
Acceptance criteria
Dependencies
std/random(RANDOM slice 1))Moved from the planning lists of the docs; the design record stays the specification.
[loop:torbscript] intake: in scope (author orion, label loop), source URI.md slice 11 and FIRST-PARTY.md gap 13, not a duplicate. Depends on #3. next: waiting (dependency #3) until 2026-10-10T04:00:00Z
[loop:torbscript] claim refine by session:loop-torbscript-20261023 until 2026-10-23T13:07Z
[loop:torbscript] decided by the loop: three technical points where URI.md section 12 does not match the code on
main(checked againststd/time,std/random,std/uriat9095875), plus the draw layout. All are internal design choices (no owner category), recorded here and to be written into URI.md section 12 by the implementing PR.at:takes aTimestamp, not anInstant. Section 12 writesUuid.version7(var source: Random, at: Instant),Ulid.at(): Instant,Ulid.generated(..., at: Instant, ...)andInstant.epoch.Instantis the monotonic reading, "from the origin the process chooses" (std/time), and has no epoch and no Unix reading, so a UUIDv7 or ULID millisecond cannot be made from it.Timestampis the wall clock in nanoseconds since 1970-01-01 UTC (Timestamp.fromUnixNanoseconds,unixNanoseconds()), is a prelude value that reads nothing, and keeps the section's point: the clock is an argument,std/identifierhas no capability. The example becomesUuid.version7(source, at: Timestamp.fromUnixNanoseconds 0). Why: the only other way is to add an epoch toInstant, which would contradict its documented meaning and widenstd/timefor no gain.Timestamp(1677 to 2262) is the only source of a negative value.version7andgeneratedpanic with a message naming the argument for aTimestampbefore the epoch, like theInt64arithmetic instd/time; they keep their plainUuid/Ulidresult. Why: aResultfor an input no clock produces would make every call site pay for a case that does not occur. The same holds forUlid.generatedwhenafteris in the same millisecond and its 80-bit random part is all ones (only reachable with a hand-built value): it panics instead of wrapping, which the ULID specification calls an error.Random's.version4takes twouint64()draws: the first fills bytes 0 to 7, the second bytes 8 to 15 (most significant first), then the version nibble (4) and the variant bits (10) are set.version7writes the 48-bit millisecond count in bytes 0 to 5, then draws twouint64()values: the top 12 bits of the first arerand_a(after the version nibble7), the top 62 bits of the second arerand_b(after the variant bits10).Ulid.generatedwrites the 48-bit millisecond count, then draws twouint64()values and uses 80 of the 128 bits for the random part (top 16 bits of the first, all 64 of the second). Why:Randompromises the same numbers per seed on every toolchain, and an identifier made from a seeded source is only reproducible (golden files, tests) if the layout is as fixed as the generator. The known-answer tests below pin it.Not decided here because it needs no decision:
Uuidkeeps exactly one conversion pair (String), sinceUrnhas noFrom<Uuid>; theDecoderule ofcompiler/src/semantics/checker/derive.trb(conversionPairOf) therefore still derives it, and a test covers the JSON round trip.[loop:torbscript] refine: dependency #3 is closed and landed on
main(std/random, PR 159, commit9e2ab91;std/randomexists withRandom.uint64(),seeded,split, stable SplitMix64 vectors). #122 is brought to the definition of ready; the decisions are in the comment above. The criteria below replace the body's "as URI.md slice 11 specifies", next: readyGoal
std/identifier: the traitIdentifier, the typesUuid(RFC 9562, versions 4 and 7) andUlid,IdentifierError, and theurn:uuid:bridge tostd/uri. A pure package: randomness and clock come in as arguments, no capability, no native.Implements
Identifier, UUIDv7 as default,Ulidkept), with the corrections in the decision comment (TimestampforInstant, panic before 1970, frozen layout).std/identifier.Seed impact
None. Pure TorbScript in a new
std/identifierpackage (use ... from "std/random","std/uri","std/time"/preludeTimestamp): no syntax change, no new native, no driver flag, no std name the compiler looks up by string. No two-commit seed rule applies.torb/sql's ownUuid(a separate repository) is not touched.Acceptance criteria
std/identifierexists (project.trbwithname = "std/identifier",src/lib.trbplus modules,tests/) and is a workspace member throughstd/*; it is not in the prelude.public trait Identifier with Show, Equals, Hash, Compare, TryFrom<String, IdentifierError>withfn bytes(): List<UInt8>(sixteen bytes, most significant first); one fixedIdentifierError { text: String, expected: String }withShow, Error.Uuid:version(): Int;static fn version4(var source: Random): Uuid;static fn version7(var source: Random, at: Timestamp): Uuid; text is the canonical 36 lower-case characters;Uuid.tryFromreads upper and lower case, refuses a wrong length, a misplaced dash and a non-hex character withIdentifierError(text, "a UUID").show()andString.from(uuid)give the canonical text.Ulid:at(): Timestamp(whole milliseconds);static fn generated(var source: Random, at: Timestamp, after: Ulid? = None): Ulid(same millisecond asafter: the random part isafter's incremented by one, otherwise drawn); text is 26 characters of Crockford base 32, upper case;tryFromreads either case, refuses a wrong length, characters outside the alphabet and a first character above7(it would overflow 128 bits) withIdentifierError(text, "a ULID").extend Uri with From<Uuid>givesurn:uuid:<canonical text>;extend Uuid with TryFrom<Urn, IdentifierError>accepts namespaceuuidwith a valid UUID (case-insensitive, as RFC 8141 makes the namespace case-insensitive) and refuses any other namespace or text.Uuidhas exactly one conversion pair (String), checked by a JSONencode/decoderound trip through the 36-character text.Compareis bytewise (so UUIDv7 and ULID of increasing milliseconds compare increasing, and theUlidtext order equals theCompareorder);Equals/Hashagree, so aUuidworks as aMapkey.std/identifier/tests/*.test.trb, run bytorb testin the VM; conformance in both back ends only if a program undertests/conformanceis added):version4andversion7fromRandom.seeded 0andRandom.seeded 1234567against fixed expected texts computed from the layout in the decision comment and the published SplitMix64 vectors; the RFC 9562 text examples parse and print unchanged;version7(source, at: Timestamp.fromUnixNanoseconds 0)starts with00000000-0000-7; theversion()nibble and the variant bits10on 1,000 generated values; the ULID specification's example01ARZ3NDEKTSV4RRFFQ69G5FAVparses and prints unchanged andat()of it is its millisecond; monotonicgeneratedwithafterincrements by exactly one in the same millisecond and draws afresh in the next; aTimestampbefore the epoch panics (testfor a panic ifstd/testhas one, otherwise documented).done, theTimestampcorrection, the frozen layout), FIRST-PARTY.md gap 13 marked done with the corrected slice number, ROADMAP row, newdocs/standard-library/identifier.mdplus its entry indocs/standard-library/index.md, and the regeneratedskills/(torb docs skill docs skills,--checkis a tier A gate).sh tools/gates.sh agreen for the PR (torb format --check, docs-skill check, pdisc, check-commits);bis not required (nocompiler/src/ir,compiler/src/backendorruntime/change).Size
One PR, two Conventional Commits:
feat(identifier): add std/identifier with Uuid, Ulid and the urn bridge(package, tests) anddocs(identifier): document std/identifier, mark URI slice 11 and FIRST-PARTY gap 13 done(or one commit if the generatedskills/stay with the package). Implementer: sonnet/medium (std/and docs). Risk to watch, not a blocker:extend Uri with From<Uuid>from another package (the same shape asextend String with From<Uri>, but across packages); if the checker rejects it, fix at the checker with a note in the PR rather than moving the bridge intostd/uri(URI.md section 7 keepsstd/urifree ofstd/identifier).Worker: RE sonnet/medium, about 90k tokens (estimate; not readable from inside the run).
[loop:torbscript] claim implement by session:loop-torbscript-20261023 until 2026-10-23T17:18Z