Skip to content

Compile the hot-shots StatsD client over UDP - #1568

Merged
ctate merged 3 commits into
mainfrom
opt/hot-shots-client
Oct 21, 2026
Merged

ctate merged 3 commits into
mainfrom
opt/hot-shots-client

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

The hot-shots package is a StatsD client that scriptc compiles from its JavaScript. Constructing a client failed. This change closes each gap on the package's UDP path generically. It adds the package as an npm test case whose program sends metrics to a local dgram server.

Gaps and fixes

  • Sloppy-mode arguments. The client's constructor and its increment and decrement methods read arguments.length in functions that have named parameters and are not in an ES module. Such a function may now read arguments wherever the aliasing of its parameters cannot show: it reads only arguments.length, or it reads elements and never writes a parameter or an element (argumentsAliasingUnobservable in arguments-usage.ts).
  • Function values in an any slot. Client.prototype.timer = function (...) through an untyped Client was refused, because the function has optional parameters. An any slot boxes the function: the box fills omitted arguments with undefined and checks the others, so the slot now admits any signature that can be boxed.
  • JSDoc on prototype methods. In a JavaScript file that TypeScript does not check, a prototype method (C.prototype.m = function (...) {}) now takes its documented parameters as checked-dynamic values, as constructors already did. hot-shots documents sendStat(..., sampleRate {Number}, tags {Array}), then passes undefined and objects to those parameters.
  • dgram sends from JavaScript. In JavaScript, socket.send() now runs the offset/length form with a sixth argument, completion callbacks, buffer lists (sent as one datagram), and sends on a connected socket. A callback receives (null, bytes), or the send failure instead of an 'error' event. The ladder takes six untyped slots and no longer needs a fence.
  • A dgram socket as an untyped value. hot-shots reads socket.emit.bind(socket) and the socket's other listener methods as values. A socket can now cross into the checked-dynamic tree as a handle (SCR_DYNH_DGRAM_SOCKET). The handle wraps an entry that holds the socket's untyped listeners, and its methods are function values that dispatch on their receiver. The handle supports on, once, removeListener, emit, send, close, bind, connect, address, ref and unref. Untyped listeners survive the socket's 'close', as Node keeps them, so the send-after-close error that hot-shots emits still reaches the listener its close() installed. JavaScript listeners with untyped parameters register through the same handle. The code is a runtime unit of its own (scr_dgram_dyn.c, the dgramDyn feature), which links only when a program converts a socket to or from an untyped value. Function.prototype.bind/call/apply on such a method value run at run time.
  • String.prototype.concat on strings, which builds the DD_ENTITY_ID tag.
  • parseInt over a possibly missing or untyped value in JavaScript, as in parseInt(process.env.DD_DOGSTATSD_PORT, 10) || 8125.

Some paths stay explicit refusals where they run: the TCP transport (socket.emit on a net.Socket), the telegraf message rewrite, and the buffered mode's Timeout handle. The Unix-socket mode reports the missing unix-dgram addon as Node does.

Evidence

  • Corpus programs 6590–6596 match Node in the plain and sanitized lanes. They cover sloppy arguments, function values in an any slot, dgram send callbacks, bound socket methods, JSDoc on prototype methods, String.prototype.concat, and parseInt over a missing value.
  • tests/fixtures/npm/cases/hot-shots-udp vendors hot-shots 8.5.0. The case compares stdout, stderr and exit status with Node in both lanes. It covers tags, explicit sample rates, the send callback, child clients, the error handler, close, a send after close, mock mode, and the Unix-socket refusal.
  • Zero-cost gate against the base commit (--corpus=all, 3450 programs): 3449 byte-identical, including every benchmark and the self-hosted compiler. One program, 2597-dgram-send-ladders.cjs, differs by intent: it is the JavaScript send-ladder program, and its calls now go to the six-slot callback-capable ladder (binary −0.03%). Benchmark time is within tolerance (geomean +0.02%). That run also showed the self-hosted compiler compiling 3.5 times slower: the dgramDyn detection walked every class's fields for each dynamic conversion. After the detection became a once-per-module fixpoint, the self set compiles to identical artifacts at +0.68% compile time (3 pairs, 95% CI −0.36% to +1.94%).

Overlap

#1324 (sloppy arguments) covers the first gap with its own implementation. Whichever of the two lands first, the other should drop its copy. A program that closes the client with promisify(client.close.bind(client))(), where the package's types resolve to any, also needs #1502 (untyped util.promisify).

🤖 Generated with Claude Code

hot-shots, a StatsD client compiled from its JavaScript, failed when a
program constructed a client. Each gap on its UDP path is closed
generically:

- Sloppy-mode functions with named parameters may read `arguments`
  where the aliasing of their parameters cannot show: only
  `arguments.length`, or element reads without parameter or element
  writes.
- An `any` slot admits any boxable function value, including functions
  with optional parameters (`C.prototype.m = function (...)` through an
  untyped receiver).
- In a JavaScript file TypeScript does not check, a prototype method
  takes its documented parameters as checked-dynamic values, as
  constructors already did.
- JavaScript dgram sends run completion callbacks, buffer lists,
  connected sends and the six-argument offset/length form.
- A dgram socket crosses into the checked-dynamic tree as a handle
  (SCR_DYNH_DGRAM_SOCKET) whose methods are function values, with
  untyped listeners that survive 'close'. The handle lives in its own
  runtime unit (scr_dgram_dyn.c, the dgramDyn feature), linked only
  when a program converts a socket to or from an untyped value.
- `String.prototype.concat` on strings, and `parseInt` over a possibly
  missing or untyped value in JavaScript.

Corpus programs 6590-6596 and the hot-shots-udp npm
case (hot-shots 8.5.0 vendored) match Node in both lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 21, 2026 2:23am UTC

Comment thread packages/runtime/src/scr_dgram_dyn.c Outdated
Comment thread packages/runtime/src/scr_dgram_dyn.c Outdated
Comment thread packages/compiler/src/frontend/arguments-usage.ts
Comment thread packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts Outdated
…n runs

concat, substr and the other typed String and Number extra methods
convert their arguments after evaluating all of them. When an argument's
conversion can run program code (an object's toString or valueOf), a
variable read of the receiver or another argument is now kept in a local
as well, so a conversion that assigns the variable leaves the
already-evaluated argument unchanged, as in Node.
@ctate
ctate merged commit 24eb054 into main Oct 21, 2026
70 of 75 checks passed
@ctate
ctate deleted the opt/hot-shots-client branch October 21, 2026 02:29

This branch was successfully deployed

1 active deployment
Preview — b811317f Deployed Oct 21, 2026 by vercel[bot]
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.

2 participants