Skip to content

EmitLiteral: render data configs as Rust source - #760

Closed
0-jake-0 wants to merge 1 commit into
closure-quotationfrom
emit-literal
Closed

EmitLiteral: render data configs as Rust source#760
0-jake-0 wants to merge 1 commit into
closure-quotationfrom
emit-literal

Conversation

@0-jake-0

@0-jake-0 0-jake-0 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #759 — base is closure-quotation, not main. Review that one first; this diff is only the EmitLiteral commit on top.

What this changes

Values can now render themselves as Rust source that reconstructs them, and a node can record its data config alongside its closure source. The emission walker in tests/emission_spike.rs consequently produces a fully self-contained artifact — one that compiles in a scope the generator does not control.

Why

The other half of what a two-pass generator needs from a wired graph, and gap 1 of the three #759's spike pinned. quote recovers closure configs because closures are erased and only their tokens survive. This covers the data configs — a ticker's Duration, a fold's seed, a limit's bound — which are not erased at all; they just have no way to say what they would look like written down.

Render the value, not its name. A lit!(PERIOD) capturing the token reads better in an artifact, and I nearly built it. It is wrong for the case the generator exists to serve: a name only resolves if the artifact is compiled where that name is bound, and the whole point of pass 1 is topology decided by running code — periods from a config file, thresholds from a database — where the values have no name at all. Rendering serves both, which is why §3 specifies it, and the same mechanism will serve §3's frozen captures ({ let thresh = 0.25f64; move |x| … }).

Impls cover §3's named set: integers (suffixed, so an unsuffixed 1 cannot infer to i32 against a u64 config), floats, bool, char, str/String, Duration, NanoTime, Option, Vec, slices, tuples to 4. Every rendered path is absolute.

How it was verified

  • cargo fmt --all
  • cargo lint ✅ — cargo lint-all not run: --all-features cannot build here (aeron needs CMake ≥3.30, box has 3.28; etcd needs protoc). Documented prerequisites, unrelated to this diff; CI is the first real check on that feature set.
  • --all-features blocked as above. Full default suite plus bench,async,csv,cache,augurs,market,ws,redis,postgres,kafka,web,kdb,prometheus,fix,dynamic-graph,tracing — green. Doctests green (10 passed).
  • New behaviour covered by tests

tests/emit_literal.rs proves the round-trip the way #759's spike proves wiring validity: each expected literal is also written out as real source in the test, so the file compiling proves the output parses and the equality proves it reconstructs. Neither assertion alone would — a string can be exactly what you expected and still not be valid Rust, or be valid Rust evaluating to something else.

Notes for the reviewer

Two formatting details are load-bearing, not stylistic:

  • Floats use {:?}, not {}Debug is the formatting specified to round-trip. 0.1 + 0.2 renders as 0.30000000000000004f64, and f64::MIN_POSITIVE survives. Non-finite values have no literal form at all and are named (::core::primitive::f64::INFINITY).
  • Duration::new(secs, nanos) rather than from_millis and friends, which only round-trip durations landing on their unit. Duration::new(2, 500_000_001) is the case that motivated it.

Absolute paths everywhere, guarded by a test. An impl relying on use statements being present at the far end would produce source that compiles in this crate and fails in a generated artifact — a failure mode that would surface only at pass 2, in code nobody wrote.

This closes gap 1 of #759's three. The walker no longer needs a caller-supplied config table, and a new test pins that the artifact contains no reference to the generator's own constants. Gaps 2 (an unquoted closure is indistinguishable from no closure) and 3 (multi-edge emission unproven) are unchanged and still pinned there.

The cost, recorded in the module docs and again here: emitting a value freezes it. The artifact carries pass 1's configuration, so changing it means regenerate and recompile. That is partial evaluation and often the point — but it is the thing that ships a stale threshold if nobody says it loudly.


Generated by Claude Code

The other half of what a two-pass generator needs from a wired graph, and
the gap the emission spike pinned. `quote` recovers *closure* configs
because closures are erased and only their tokens survive; this covers the
*data* configs — a ticker's `Duration`, a fold's seed, a limit's bound —
which are not erased at all, they just have no way to say what they would
look like written down.

Render the value, not its name. A `lit!(PERIOD)` capturing the token would
read better in an artifact, but a name only resolves if the artifact is
compiled where that name is bound — and the whole point of pass 1 is
topology decided by running code, where the values come from a config file
and have no name at all. Rendering serves both, which is why §3 specifies
it, and the same mechanism will serve §3's frozen captures.

Impls cover §3's named set — integers (suffixed, so an unsuffixed `1` in
an artifact cannot infer to `i32` against a `u64` config), floats, bool,
char, `str`/`String`, `Duration`, `NanoTime`, `Option`, `Vec`, slices,
tuples to 4. Every rendered path is absolute, because a generated file is
compiled in a scope the generator does not control.

Two details worth the reading:

- Floats use `{:?}`, not `{}` — `Debug` is the formatting *specified* to
  round-trip. Non-finite values have no literal form and are named
  (`::core::primitive::f64::INFINITY`).
- `Duration::new(secs, nanos)` rather than `from_millis` and friends,
  which only round-trip durations landing on their unit.

`tests/emit_literal.rs` proves the round-trip the way the emission spike
proves wiring validity: each expected literal is *also written out as real
source* in the test, so the file compiling proves the output parses and
the equality proves it reconstructs. Neither assertion alone would.

Wired into the graph via `Stream::with_cfg` + `NodeInfo::cfg_src`, so the
walker in `tests/emission_spike.rs` no longer needs a caller-supplied
table — and the artifact it emits is now fully self-contained, carrying
`Duration::new(0, 1_000_000)` instead of depending on a `PERIOD` constant
being in scope at the far end. That closes gap 1 of the three the spike
recorded; a test now pins the self-containment.

The cost is the one §3 names loudly and this records again: emitting a
value **freezes** it. The artifact carries pass 1's configuration, so
changing it means regenerate and recompile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X4eojqRWBJ6uK5v5JDzmkd

0-jake-0 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #769, which squashes this whole stack (#759#768) onto main as a single reviewable change.

EmitLiteral and its round-trip tests are carried forward in full. Closing to keep the queue readable; no content is being dropped.


Generated by Claude Code

@0-jake-0 0-jake-0 closed this Aug 8, 2026
@0-jake-0
0-jake-0 deleted the emit-literal branch August 9, 2026 23:07
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