polydat_core/kernel/opt.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `KernelOptLevel` โ session-wide optimization knob for op-template
5//! kernel synthesis.
6//!
7//! The closure-binding economy (Rule 5) allocates a magic extern
8//! (`body` / `count` / `ok`) only when the result source references
9//! it (subcontext_construction.md ยง3.2), so a kernel has no slot for
10//! an unreferenced one and a host write to that name finds none.
11//!
12//! That's fine in production: the workload didn't ask for the value,
13//! we don't pay to track it. It's actively harmful for step-debug /
14//! cycle-replay / "show me what the adapter actually wrote this
15//! cycle even though nothing read it" inspection.
16//!
17//! `Diagnostic` mode allocates all three magic externs whether or not
18//! the source references them. The writes land, `wires.get` answers,
19//! the step-debugger sees the real values. Compute path unchanged โ
20//! the extra slots have no eval cone hanging off them.
21//!
22//! The knob threads through [`crate::kernel::subcontext::CompileOptions`]
23//! and is consulted by `SubcontextBuilder::add_result_bindings`. The
24//! polydat binary does not expose it; a host may map a CLI flag onto it.
25
26/// Optimization level for op-template kernel synthesis.
27///
28/// `Release` is the production default โ closure-binding economy
29/// elides magic-extern slots nothing references. `Diagnostic` keeps
30/// every magic-extern slot allocated so step-debug / cycle-replay
31/// introspection can see the values the runtime would otherwise
32/// drop.
33///
34/// Naming: matches the rustc convention (`opt-level=0..3`) but
35/// collapsed to two semantically-distinct positions; there's no
36/// useful middle ground between "DCE on" and "keep everything for
37/// inspection."
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
39pub enum KernelOptLevel {
40 /// Production default. Closure-binding economy elides slots
41 /// for unreferenced magic externs; an elided name has no slot
42 /// to write.
43 #[default]
44 Release,
45 /// Step-debug / cycle-replay mode. Force-allocate every magic
46 /// extern (`body` / `count` / `ok`) regardless of reference.
47 /// Runtime writes always land; `wires.get` always answers.
48 Diagnostic,
49}
50
51impl KernelOptLevel {
52 /// True when slot allocation should ignore the "is this name
53 /// referenced?" check and force-allocate every candidate slot.
54 pub fn keep_unreferenced_slots(self) -> bool {
55 matches!(self, Self::Diagnostic)
56 }
57
58 /// Parse from a CLI-style string. Returns `Err(input)` on an
59 /// unrecognised value so the caller can format its own
60 /// diagnostic.
61 pub fn parse(s: &str) -> Result<Self, &str> {
62 match s {
63 "release" => Ok(Self::Release),
64 "diagnostic" => Ok(Self::Diagnostic),
65 _ => Err(s),
66 }
67 }
68
69 /// Canonical CLI spelling for this level. Inverse of
70 /// [`Self::parse`].
71 pub fn as_str(self) -> &'static str {
72 match self {
73 Self::Release => "release",
74 Self::Diagnostic => "diagnostic",
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn default_is_release() {
85 assert_eq!(KernelOptLevel::default(), KernelOptLevel::Release);
86 }
87
88 #[test]
89 fn keep_unreferenced_slots_release() {
90 assert!(!KernelOptLevel::Release.keep_unreferenced_slots());
91 }
92
93 #[test]
94 fn keep_unreferenced_slots_diagnostic() {
95 assert!(KernelOptLevel::Diagnostic.keep_unreferenced_slots());
96 }
97
98 #[test]
99 fn parse_roundtrip() {
100 for lvl in [KernelOptLevel::Release, KernelOptLevel::Diagnostic] {
101 assert_eq!(KernelOptLevel::parse(lvl.as_str()), Ok(lvl));
102 }
103 }
104
105 #[test]
106 fn parse_unknown_returns_err() {
107 assert_eq!(KernelOptLevel::parse("none"), Err("none"));
108 assert_eq!(KernelOptLevel::parse(""), Err(""));
109 }
110}