Skip to content

fix: trim-compatibility for Router registration, the precompile workload, and one error path#1330

Open
quinnj wants to merge 8 commits into
masterfrom
trim-verifier-fixes
Open

fix: trim-compatibility for Router registration, the precompile workload, and one error path#1330
quinnj wants to merge 8 commits into
masterfrom
trim-verifier-fixes

Conversation

@quinnj

@quinnj quinnj commented Jul 11, 2026

Copy link
Copy Markdown
Member

Three small changes that make HTTP servers compilable with juliac --trim:

1. register! / Router: force specialization on handler/middleware arguments

Julia doesn't specialize on function-valued arguments that are only passed through (not called), so every registration funneled into one handler::Function instance — where the middleware wrap and the parametric Leaf/Router constructions are runtime apply_type calls, unresolvable under --trim (6 verifier errors in a real server graph). Adding ::F ... where F to the register! forms and the Router convenience constructor is dispatch-neutral and makes each registration site compile concretely.

Also: build segments as a Union{String,Variable} container instead of map (whose eltype widens to Any here) — otherwise every segment comparison inside insert! is a dynamic ==.

2. HTTP_PRECOMPILE_WORKLOAD=0 opt-out for the precompile workload

The workload starts real servers and requests, which must not execute inside a static trim compilation (observed: segfault when the workload runs under juliac --trim=safe). An explicit env opt-out lets build pipelines disable it without patching the package.

3. Don't interpolate plan.mode into the HTTP/2 proxy ArgumentError

Enum show machinery is a dynamic site under --trim; the static message carries the same information for this error class.

Validation

  • test/http_handlers_tests.jl green on this branch.
  • A real HTTP server application graph (30+ packages) compiles through juliac --trim=safe against this branch with zero HTTP-rooted verifier errors (down from 6/12), with registration through middleware-wrapped closures and two routers.
  • No dispatch behavior changes — signatures are equivalent for method selection; only specialization changes.

🤖 Generated with Claude Code

… and one error path

Three small changes that make HTTP usable in juliac --trim builds:

- register!/Router: add type parameters to the handler/middleware
  arguments. Julia doesn't specialize on function-valued arguments that
  are only passed through, so every registration funneled into one
  handler::Function instance where the middleware wrap and parametric
  Leaf/Router construction are runtime apply_type calls — unresolvable
  under --trim (6 verifier errors in a real server graph).
- precompile workload: HTTP_PRECOMPILE_WORKLOAD=0 env opt-out. The
  workload starts real servers/requests, which segfaults when executed
  inside a static trim compilation.
- http_client: don't interpolate plan.mode into the HTTP/2 proxy
  ArgumentError — enum show machinery is a dynamic site under --trim.
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.62500% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.07%. Comparing base (25388ae) to head (1eac54e).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/http2_server.jl 69.56% 14 Missing ⚠️
src/http_handlers.jl 72.91% 13 Missing ⚠️
src/http_core.jl 74.41% 11 Missing ⚠️
src/precompile.jl 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1330      +/-   ##
==========================================
+ Coverage   88.04%   88.07%   +0.02%     
==========================================
  Files          30       30              
  Lines       11849    12016     +167     
==========================================
+ Hits        10433    10583     +150     
- Misses       1416     1433      +17     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

quinnj added 5 commits July 11, 2026 14:12
The h2 GOAWAY request was the only shutdown hook ever installed, but the
Union{Nothing,Function} field made every hook invocation dynamic dispatch —
unresolvable under juliac --trim. Hoist _H2ServerConnControl ahead of
_ServerConn and store the hook as a concrete callable struct instead of a
closure.
Deep middleware graphs widen the handler's response type (inference-budget
truncation), turning the response-write calls into runtime specialization
dispatch. _write_all_response!/write_response!/_write_h2_response! (and the
h2 streaming helper) now take @nospecialize(response) — one instance covers
every Response{B} — with an explicit isa chain over the body shapes in
write_response! and socket-union isa splits (with per-branch typeasserts,
which prevent the optimizer from tail-merging the branches back into one
dynamic call) at the h1 call sites.
With the response @nospecialize'd, every body read is abstract; concrete-first
isa chains (String / SubString{String} / Vector{UInt8} / BytesBody{Vector{UInt8}}
/ EmptyBody, then the abstract residuals) keep the common paths statically
dispatched under juliac --trim. The h2 fast path normalizes buffered bodies to
a concrete Vector{UInt8} once (one copy for string bodies; the frame writer
copies into the connection buffer anyway). Shared _body_close_any! replaces the
scattered widened close sites. Static messages in the unsupported-body throws
(typeof interpolation is show machinery — dynamic under trim).
…nospecialized write paths

Nospecializing the write path traded 4 boundary errors for ~90 interior ones
(every body read abstract) and the contract-tightening attempt broke range
serving. Instead: the write paths stay fully specialized per Response{B}
(clean under --trim, verified in isolation), and thin @noinline shims at the
two server boundaries isa-chain over the concrete Response shapes servers
produce — each branch dispatching into the specialized path, one residual
dynamic arm for exotic body types.
…im workloads

Review feedback (PR #1330): the per-site isa chains were fragile — easy to
miss a branch or let sites drift apart. Two @inline helpers are now the
single source of truth for the concrete shapes the server write paths
dispatch over:

- _with_body_narrowed(f, body): nothing / String / SubString{String} /
  Vector{UInt8} / EmptyBody / BytesBody{Vector{UInt8}} / AbstractBody
  (streaming residual) / fallback. Every branch re-narrows so f's call is
  statically dispatched. Used by _response_has_body (via shared
  _response_body_known_empty methods, also reused for the h2 body_empty
  check), the h1 chunked + exact-body writes, the h2 byte normalization,
  the h2 streaming writer, and _body_close_any!.
- _with_response_narrowed(f, response): the Response{B} equivalent, used
  by both _write_all_response_dyn! / _write_h2_response_dyn! shims.

The h2 streaming writer's isa ladder became per-shape _write_h2_body_shape!
methods (concrete b from the helper keeps their selection static).

New trim-compile workloads in the existing harness (both zero-tolerance,
compile + run under stock juliac):
- http_trim_server_response_shapes.jl: every supported body shape served
  and verified over the wire, through the widened-response shims.
- http_trim_server_router_registration.jl: Router construction + every
  register! form incl. middleware wrap and wildcard/param paths. Serving
  THROUGH the router stays uncovered pending the router-dispatch design.

Full-graph verifier count unchanged; h1+h2 server testsets green.
@quinnj

quinnj commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Both review points addressed in 26c9b73:

Consolidated narrowing helpers — the per-site isa chains are gone. Two @inline helpers in http_core are now the single source of truth for the covered type set:

  • _with_body_narrowed(f, body) — the body shapes (nothing/String/SubString{String}/Vector{UInt8}/EmptyBody/BytesBody{Vector{UInt8}}/AbstractBody residual/fallback). Each branch re-narrows so f's call is statically dispatched; sites pass a closure (or a multi-method function like the new _write_h2_body_shape! shapes) with their site-specific logic. Used by _response_has_body, the h1 chunked/exact writes, h2 emptiness + byte normalization + streaming writer, and _body_close_any!.
  • _with_response_narrowed(f, response) — the Response{B} equivalent, shared by both boundary dispatch shims.

Adding a supported shape is now one branch in one place.

Trim-compile tests — turns out master already had the trim harness + 13 client workloads, so I added two server-write workloads to it (both zero-tolerance: compile with 0 verifier errors AND run the binary with wire-level assertions, under stock juliac):

  • http_trim_server_response_shapes.jl: serves every supported body shape and verifies each over a raw exchange — this exercises the widened-response shims, both narrowing helpers, and the h1 write chains end-to-end.
  • http_trim_server_router_registration.jl: Router construction + every register! form (middleware wrap, wildcard/param paths, do-block form). Serving through the router is deliberately not covered yet — that's the handler-table dispatch frontier your router-dispatch design will address; the workload comment marks where the serving case should go once it lands.

Validation: both new workloads pass locally (HTTP_TRIM_ONLY=… runs them in ~10s each thanks to juliac caching), h1+h2 server testsets green, and the downstream full-server graph's verifier count is unchanged by the refactor.

🤖 Generated with Claude Code

quinnj and others added 2 commits July 13, 2026 08:40
… pattern)

The matched-handler invocation was runtime dispatch over the open set of
registered handler closure types — the one remaining trim frontier in the
server. Leaf now stores a HandlerFn: a per-callable-type @generated
@cfunction whose first C argument is the callable itself (Ref{F}), so the
handler call inside the trampoline is concretely dispatched, invocation is
a ccall through the stored pointer (statically resolvable), and _root keeps
the callable alive. The handler table stays runtime-mutable and register!
is unchanged.

Two constraints discovered and encoded:
- the trim verifier rejects boxed-Any cfunction signatures, so the request
  and result cross the trampoline as raw pointers to caller-GC.@preserve'd
  Ref{Any} boxes (primitive-only C signature);
- an Any-typed call of the callable is itself specialization dispatch, so
  _call_handler_narrowed isa-narrows over the closed set of server-side
  request/stream shapes before invoking.

getparams/getparam: typed via an isa narrow of the context fetch (their
Any-typed returns made every param use downstream dynamic).

The router trim workload now serves THROUGH the router (params, wildcards,
404/405 verified over the wire) — the frontier case is covered and
zero-tolerance under the stock harness.
…trim coverage

- "trim_strict_bodies" Preference (default off): juliac --trim builds opt
  into strict narrowing — unknown request/response body shapes throw instead
  of taking the residual dynamic-dispatch fallback, letting the verifier
  prove the narrowing helpers fully static. A Preference rather than an ENV
  switch so flipping it invalidates the precompile cache (same pattern as
  StructUtils' trim_specialize).
- narrowing chains gain the BytesBody{Base.CodeUnits{UInt8,String}} arms —
  what the compat constructors produce for string bodies; previously these
  fell through to the AbstractBody/dynamic arms, which strict mode forbids
- response-shapes trim workload gains the 2-arg Response(status, body) form
  (the common error-response pattern), and the harness gains a strict-mode
  case: the same workload compiled in a temp project with the preference on
  (HTTP_TRIM_ONLY=strict selects just this case)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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