Skip to content

fix: trim-resolvable UID construction - #1

Merged
quinnj merged 5 commits into
mainfrom
trim-verifier-fixes
Jul 15, 2026
Merged

fix: trim-resolvable UID construction#1
quinnj merged 5 commits into
mainfrom
trim-verifier-fixes

Conversation

@quinnj

@quinnj quinnj commented Jul 11, 2026

Copy link
Copy Markdown
Member

Three commits that make UID generation compile cleanly under juliac --trim (found while trim-compiling an HTTP server whose id generation funnels through Encid):

1. Specialize the constructors on the uid type

Julia doesn't specialize methods on unparameterized ::Type{<:Union{...}} arguments, and kwarg NamedTuples type a Type value as a bare DataType — either way every UID(...) call funneled into one instance where uid_type is a runtime DataType: type-unstable, and unresolvable dynamic dispatch under --trim. Both entry points now take ::Type{U} ... where U and forward positionally to _uid_impl, so the body's uid_type == UIDn ladder folds per instance.

2. Value-level String fast path (single-word uid types)

The generic path wraps strings as StringN{length(s)} — a runtime type parameter — which makes the whole encode machinery runtime dispatch. Generation only needs the length's value, so UID(::Type{U}, s::String) for U ∈ {UID2, UID4, UID8, UID16} does the same bit surgery value-level. Bit-identical output verified seed-for-seed against the generic path for all four types. Multi-word types fall through to the generic method unchanged. Note: the result is tagged UID{String, U} rather than UID{Tuple{StringN{N}}, U} — printing/generation identical; callers that decode by type should use the generic constructor.

3. base58: byte fill instead of "1"^n

repeat(::String, ::Int) is an unresolved invoke under --trim; String(fill(UInt8('1'), n)) produces the same padding (roundtrip verified, e.g. [0,0,5,1] → "11P6" → [0,0,5,1]).

Found in passing (not addressed here)

The generic path throws for UID24 with a 16-char string: its words vector [UInt128(t[1]), UInt64(t[2])] promotes to Vector{UInt128}, and the UID24((words[1], words[2])) construction then rejects (UInt128, UInt128). Pre-existing; worth its own fix.

Tests green; with this + the JSON/StructUtils/Parsers/HTTP trim PRs, a 30+ package server graph's id-generation path contributes zero verifier errors.

🤖 Generated with Claude Code

quinnj added 5 commits July 11, 2026 12:42
Julia doesn't specialize methods on unparameterized Type arguments
(::Type{<:Union{...}}), so every UID(...; uid_type=...) call funneled into
one instance where uid_type is a runtime DataType — type-unstable, and
unresolvable dynamic dispatch under juliac --trim. A where-param keeps
dispatch identical and makes each uid_type a compile-time constant (the
if-else ladder in the body folds per instance).
…evel String fast path)

- UID constructors take the uid type as a where-param and forward to
  _uid_impl positionally: julia doesn't specialize on unparameterized
  Type arguments, and kwarg NamedTuples type a Type value as a bare
  DataType — either way every call funneled into one instance with a
  runtime DataType, unresolvable under juliac --trim.
- Single-word String fast path: the generic path wraps strings as
  StringN{length(s)} — a runtime type parameter — making the encode
  machinery runtime dispatch. The value-level path produces bit-identical
  results (verified seed-for-seed against the generic path for
  UID2/4/8/16) and tags the result UID{String, U}. Multi-word types fall
  through to the generic method.

Found in passing: the generic path throws for UID24 with a 16-char
string (its words vector promotes to Vector{UInt128}, then the UID24
constructor rejects (UInt128, UInt128)) — pre-existing, not addressed
here.
repeat(String, Int) is an unresolved invoke under juliac --trim; an
explicit byte fill produces the same string.
Compiles test/encid_trim_workload.jl with juliac --trim=safe (JuliaC.jl, error
budget zero) and runs the resulting executable, so the trim-verifier fixes in
this PR can't silently regress. Same harness shape as StructUtils/HTTP.

The workload asserts runtime values across the trim-safe surface: base58
encode/decode incl. the leading-zero padding path, primitive uid construction
and rendering, and the value-level UID(U, ::String) fast path for every
single-word uid type. The generic args... machinery (runtime StringN{N}) and
decode-by-index (runtime type-parameter walk) are documented as outside the
trim-safe surface and stay covered by the regular suite.

Skips on Julia < 1.12 and on prerelease builds.
The multi-word primitive uid constructors (UID24/UID32/UID64) reinterpret
NTuple values into wide primitive types, which Julia only supports from 1.10
— on 1.6-1.9 the bitcast rejects tuple arguments, so the basics testset has
failed the 'min' CI job since the initial commit (main included). 1.10 is the
current LTS; the full suite passes there (the trim-compile testset skips
below 1.12 by design).
@quinnj
quinnj merged commit bf455ce into main Jul 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant